mirror of
https://github.com/discourse/discourse.git
synced 2026-09-05 04:40:41 -05:00
DEV: Add a UI kit developer guide (#43023)
There was no guidance on ui-kit anywhere in the developer docs or in `AI-AGENTS.md`, which only pointed at FormKit. This adds `docs/developer-guides/docs/03-code-internals/02-ui-kit.md`. The guide explains what lives in `frontend/discourse/app/ui-kit` (components, helpers, modifiers), how to import it, and why `-internals` directories are off limits. It says when to reuse a primitive versus writing a domain component, and where FormKit and float-kit take over. A grouped map of the current primitives links to their styleguide sections. Every description was checked against the source, and deprecated primitives are left out. For contributors to the kit it sets the rules: naming, TypeScript signatures with TSDoc, `...attributes`, BEM and tokens, strings passed in already translated, and how to split a large primitive into an entry module plus `types.ts` and `-internals/` collaborators. A section on blast radius asks for backward-compatibility analysis and a deprecation cycle for any public-facing change, and exhaustive tests for anything new. The styleguide section is treated as part of the deliverable, with the drag-and-drop page as the model. `AI-AGENTS.md` now tells agents to build UI from the kit's primitives. The guide sits right after the Ember components tutorial, so the other code-internals guides are renumbered by one and every filename reference in docs, skills, and TSDoc is updated.
This commit is contained in:
@@ -62,7 +62,7 @@ Current examples:
|
||||
|
||||
Use FormKit for new admin forms. Do not build ad hoc form controls when FormKit components cover the interaction. Keep client validation paired with server-side validation; do not rely only on browser or JS validation.
|
||||
|
||||
Reference: `docs/developer-guides/docs/03-code-internals/21-form-kit.md` and `frontend/discourse/app/form-kit`.
|
||||
Reference: `docs/developer-guides/docs/03-code-internals/22-form-kit.md` and `frontend/discourse/app/form-kit`.
|
||||
|
||||
New/edit forms usually belong on third-level routes rather than inline inside a table row. This makes the form linkable, reloadable, and easier to test.
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ name: discourse-service-authoring
|
||||
description: Use when creating, editing, or reviewing Discourse service objects that include Service::Base - covers contracts, models, policies, steps, transactions, controller integration, and service specs
|
||||
---
|
||||
|
||||
We want high quality code and very senior engineering work. Best oriented object practices are observed. Think principles like SOLID and battle-tested patterns. We also want to write idiomatic ruby. Good reference authors are Sandi Metz, Katrina Owen or Avdi Grimm. Your only source of truth to write services is the documentation at docs/developer-guides/docs/03-code-internals/19-service-objects.md, don't look at examples in the codebase.
|
||||
We want high quality code and very senior engineering work. Best oriented object practices are observed. Think principles like SOLID and battle-tested patterns. We also want to write idiomatic ruby. Good reference authors are Sandi Metz, Katrina Owen or Avdi Grimm. Your only source of truth to write services is the documentation at docs/developer-guides/docs/03-code-internals/20-service-objects.md, don't look at examples in the codebase.
|
||||
|
||||
DONT USE ANY WRITING PLAN SKILL DURING THE SESSION
|
||||
|
||||
@@ -180,7 +180,7 @@ Two authoritative references govern how specs are written. Whenever you are unsu
|
||||
1. **RSpec Style Guide** — https://rspec.rubystyle.guide
|
||||
Fetch this page and search for the keyword you need guidance on (e.g. "subject", "context", "let", "shared examples", "named subject", "aggregate_failures", "one expectation"). Use it as the definitive authority on RSpec idioms and style.
|
||||
|
||||
2. **Service documentation** — `docs/developer-guides/docs/03-code-internals/19-service-objects.md`, section **Testing**
|
||||
2. **Service documentation** — `docs/developer-guides/docs/03-code-internals/20-service-objects.md`, section **Testing**
|
||||
This is the definitive authority on testing Discourse services: structure, custom matchers, and conventions. Every spec must follow the patterns shown there.
|
||||
|
||||
### Checklist
|
||||
|
||||
@@ -18,9 +18,9 @@ Two CSS rules are the most load-bearing — get them right by reflex:
|
||||
These rules operationalize Discourse's documented frontend philosophy — **mobile-first,
|
||||
progressive enhancement (works without hover or JS), a themeable base layer, and a shared design
|
||||
system over bespoke styling.** The two source-of-truth docs are
|
||||
[`25-css-guidelines-bem.md`](../../docs/developer-guides/docs/03-code-internals/25-css-guidelines-bem.md)
|
||||
[`26-css-guidelines-bem.md`](../../docs/developer-guides/docs/03-code-internals/26-css-guidelines-bem.md)
|
||||
(naming) and
|
||||
[`27-designing-for-devices.md`](../../docs/developer-guides/docs/03-code-internals/27-designing-for-devices.md)
|
||||
[`28-designing-for-devices.md`](../../docs/developer-guides/docs/03-code-internals/28-designing-for-devices.md)
|
||||
(responsive / device adaptation). The canonical real-world example is the chat loading skeleton —
|
||||
[`plugins/chat/assets/javascripts/discourse/components/chat-skeleton.gjs`](../../plugins/chat/assets/javascripts/discourse/components/chat-skeleton.gjs)
|
||||
and its `.scss`.
|
||||
@@ -373,7 +373,7 @@ Discourse templates are **`.gjs`** (Glimmer components with inline `<template>`)
|
||||
(`<form.Field>`, `<form.Row>`, `<form.Submit>`) and handles layout, validation, state, and the
|
||||
label/error/a11y wiring for you. Don't hand-assemble a raw `<form>` with manual `<input>`s and
|
||||
bespoke validation. See
|
||||
[`docs/developer-guides/docs/03-code-internals/21-form-kit.md`](../../docs/developer-guides/docs/03-code-internals/21-form-kit.md)
|
||||
[`docs/developer-guides/docs/03-code-internals/22-form-kit.md`](../../docs/developer-guides/docs/03-code-internals/22-form-kit.md)
|
||||
(`frontend/discourse/app/form-kit`).
|
||||
- **Splat `...attributes` on the component's root element** so a caller can pass a class,
|
||||
`data-*`, `aria-*`, or a `--modifier` through. Without it the component is a closed box. The
|
||||
@@ -391,7 +391,7 @@ Discourse templates are **`.gjs`** (Glimmer components with inline `<template>`)
|
||||
a **public API surface and maintenance commitment** — once it exists, extensions depend on its
|
||||
name and `@outletArgs`, so it can't be moved freely. Add one only for a concrete need; pass
|
||||
data via `lazyHash` (not `hash`) and name it by location (`above-…`, `below-…`). See
|
||||
[`13-plugin-outlet-connectors.md`](../../docs/developer-guides/docs/03-code-internals/13-plugin-outlet-connectors.md).
|
||||
[`14-plugin-outlet-connectors.md`](../../docs/developer-guides/docs/03-code-internals/14-plugin-outlet-connectors.md).
|
||||
- **Heading levels follow the document outline, not type size.** Never pick a level for its
|
||||
default font size — if the right heading looks wrong-sized, style it in CSS
|
||||
(`font-size: var(--font-up-1)`). An `<h1>` styled smaller is fine; an `<h3>` chosen because
|
||||
@@ -443,7 +443,7 @@ it in the matching `_index.scss` / parent `@import` (partials are underscore-pre
|
||||
|
||||
- **Write one responsive stylesheet, not desktop + mobile copies.** Discourse designs
|
||||
**mobile-first** and enhances upward (see the philosophy doc,
|
||||
[`27-designing-for-devices.md`](../../docs/developer-guides/docs/03-code-internals/27-designing-for-devices.md)):
|
||||
[`28-designing-for-devices.md`](../../docs/developer-guides/docs/03-code-internals/28-designing-for-devices.md)):
|
||||
new styles live in `common/` and adapt with breakpoints. **Prefer intrinsic layout** (e.g.
|
||||
`grid-template-columns: repeat(auto-fill, minmax(14em, 1fr))`) and reach for a breakpoint only
|
||||
to *restructure*; use the `lib/viewport` mixins (`viewport.from`/`until`/`between`). The legacy
|
||||
|
||||
@@ -5,7 +5,7 @@ write one responsive stylesheet (not desktop/mobile copies), prefer intrinsic la
|
||||
`lib/viewport` for breakpoints. This file is the detail.
|
||||
|
||||
The authoritative philosophy is
|
||||
[`docs/developer-guides/docs/03-code-internals/27-designing-for-devices.md`](../../../docs/developer-guides/docs/03-code-internals/27-designing-for-devices.md):
|
||||
[`docs/developer-guides/docs/03-code-internals/28-designing-for-devices.md`](../../../docs/developer-guides/docs/03-code-internals/28-designing-for-devices.md):
|
||||
**design mobile-first, then enhance for larger viewports and richer input** — read it for the
|
||||
full picture.
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ and
|
||||
[`frontend/discourse/float-kit/components/d-tooltip.gts`](../../frontend/discourse/float-kit/components/d-tooltip.gts)
|
||||
(typed `@service declare` injection, `Element`, several documented `Blocks`, an option bag
|
||||
derived from a shared type); the developer guide is
|
||||
[`26-types.md`](../../docs/developer-guides/docs/03-code-internals/26-types.md). **The
|
||||
[`27-types.md`](../../docs/developer-guides/docs/03-code-internals/27-types.md). **The
|
||||
global tsconfig is deliberately NOT strict** (it extends the repo-root `tsconfig-base.json`,
|
||||
which sets no `strict` and no `checkJs`); tightening it is a separate, repo-wide effort.
|
||||
|
||||
|
||||
+4
-3
@@ -15,9 +15,10 @@ Discourse is large with long history. Understand context before changes.
|
||||
|
||||
### JavaScript and UI
|
||||
- No empty backing classes for template-only components unless requested
|
||||
- Build UI from the ui-kit primitives (`frontend/discourse/app/ui-kit`: `d-*` components, `helpers/`, `modifiers/`) rather than hand-rolling controls, layout, or behaviour; new shared, domain-free primitives go there. See ./docs/developer-guides/docs/03-code-internals/02-ui-kit.md
|
||||
- Use the skill at `.skills/discourse-frontend-conventions` when writing or reviewing JS/TS/Glimmer classes and templates (private members, member ordering, comments, invocation ordering)
|
||||
- Use FormKit for forms, see ./docs/developer-guides/docs/03-code-internals/21-form-kit.md (`frontend/discourse/app/form-kit`)
|
||||
- Use BEM for CSS, see ./docs/developer-guides/docs/03-code-internals/25-css-guidelines-bem.md
|
||||
- Use FormKit for forms, see ./docs/developer-guides/docs/03-code-internals/22-form-kit.md (`frontend/discourse/app/form-kit`)
|
||||
- Use BEM for CSS, see ./docs/developer-guides/docs/03-code-internals/26-css-guidelines-bem.md
|
||||
- Make display strings translatable (use placeholders, not split strings)
|
||||
- Use "Sentence case" for strings, not "Proper Case" or "lower case"
|
||||
- Plugins/themes can't import npm modules directly; add the dependency to core and expose a `frontend/discourse/app/lib/load-*.js` wrapper that does the `import()` (see `load-morphlex.js`).
|
||||
@@ -60,7 +61,7 @@ use `bin/lint --fix --recent` only when appropriate.
|
||||
|
||||
## Services
|
||||
- Extract business logic (validation, models, permissions) from controllers
|
||||
- docs/developer-guides/docs/03-code-internals/19-service-objects.md
|
||||
- docs/developer-guides/docs/03-code-internals/20-service-objects.md
|
||||
- Use the skill at .skills/discourse-service-authoring
|
||||
- Examples: `app/services` (only classes with `Service::Base`)
|
||||
|
||||
|
||||
@@ -8,6 +8,8 @@ In the [previous tutorial](https://meta.discourse.org/t/creating-routes-in-disco
|
||||
|
||||
We now recommend you to read the Ember component documentation: https://guides.emberjs.com/release/components/introducing-components/
|
||||
|
||||
Before writing a component, check the [UI kit](02-ui-kit.md) of shared components, helpers, and modifiers; most common controls already exist there.
|
||||
|
||||
[details="Old tutorial"]
|
||||
In this tutorial, I’m going to create a new [Ember Component](https://guides.emberjs.com/v2.7.0/components/defining-a-component/) as a way to wrap third party Javascript. This is going to be similar to a [YouTube](https://www.youtube.com/watch?v=S_l_DL8ysQQ) video I made a while back, which you may find informative, only this time it’s specific to Discourse and how we lay out files in our project.
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: The UI kit of shared Discourse components, helpers, and modifiers
|
||||
short_title: UI kit
|
||||
id: ui-kit
|
||||
---
|
||||
|
||||
<div data-theme-toc="true"> </div>
|
||||
|
||||
`ui-kit` is the layer of reusable, domain-free building blocks that core, plugins, and themes compose their interfaces from. It lives under `frontend/discourse/app/ui-kit/` and ships three kinds of primitive:
|
||||
|
||||
- **Components**: `d-button`, `d-modal`, `d-select`, `d-skeleton`, and so on.
|
||||
- **Helpers** (`ui-kit/helpers/`): `d-icon`, `d-format-date`, `d-concat-class`, `d-user-avatar`, and so on.
|
||||
- **Modifiers** (`ui-kit/modifiers/`): `d-trap-tab`, `d-on-resize`, `d-close-on-click-outside`, the drag-and-drop family, and so on.
|
||||
|
||||
Everything in it is prefixed with `d-` (`D` in PascalCase) and is considered public API for plugins and themes. This page explains when to reach for it, what is in it, and how to add to it. Each primitive's own JSDoc/TSDoc is the reference for its arguments; the [interactive styleguide](https://meta.discourse.org/styleguide) shows most of them rendered.
|
||||
|
||||
# Importing
|
||||
|
||||
Import from the `discourse/ui-kit` namespace:
|
||||
|
||||
```gjs
|
||||
import { on } from "@ember/modifier";
|
||||
import DButton from "discourse/ui-kit/d-button";
|
||||
import dIcon from "discourse/ui-kit/helpers/d-icon";
|
||||
import dAutoFocus from "discourse/ui-kit/modifiers/d-auto-focus";
|
||||
|
||||
<template>
|
||||
<label class="rename-form__label">{{dIcon "pencil"}} {{@label}}</label>
|
||||
<input
|
||||
class="rename-form__input"
|
||||
value={{@name}}
|
||||
{{dAutoFocus selectText=true}}
|
||||
{{on "input" @onInput}}
|
||||
/>
|
||||
<DButton
|
||||
class="btn-primary"
|
||||
@action={{@onSave}}
|
||||
@label="save"
|
||||
@icon="check"
|
||||
@isLoading={{@isSaving}}
|
||||
/>
|
||||
</template>
|
||||
```
|
||||
|
||||
`discourse/components/*`, `discourse/helpers/*`, and `discourse/modifiers/*` are still where domain-specific, non-shareable components live; only the reusable primitives were moved into the kit. The old paths of the moved ones (`discourse/components/d-button`, `discourse/helpers/d-icon`, and so on) still resolve through the shims in `frontend/discourse/app/ui-kit-shims.js`, so existing plugins keep working, but new code should import them from `discourse/ui-kit/...` directly.
|
||||
|
||||
Do not import anything from a `-internals` directory, wherever it appears (`discourse/ui-kit/-internals/...`, `discourse/lib/-internals/...`, and so on). Those modules are private implementation details of the primitives that wrap them: they are not part of the public API and can be renamed, reshaped, or removed without notice or a deprecation cycle. If a primitive does not expose what you need, extend the primitive rather than reaching past it.
|
||||
|
||||
# When to use it
|
||||
|
||||
Before writing a component, check whether the kit already covers the need:
|
||||
|
||||
1. Browse the `ui-kit/` directory (component, helper, and modifier names are descriptive) and the [styleguide](https://meta.discourse.org/styleguide) sections.
|
||||
2. Prefer composing existing primitives over adding a variant. A `d-button` with `@icon` and a `class` attribute is better than a new button component; a `d-empty-state` with a custom `@ctaLabel` is better than a bespoke empty message.
|
||||
3. If the need is a **form**, use [FormKit](22-form-kit.md) rather than assembling inputs by hand; FormKit's controls are themselves built on ui-kit primitives.
|
||||
4. If the need is **anchored or hover-triggered UI** (tooltips, menus, popovers), use float-kit rather than positioning things yourself. It ships both components (`DTooltip`, `DMenu`, `DPopover`, `DToast`, and their headless variants, imported from `discourse/float-kit/components/...`) and the `tooltip`, `menu`, and `toasts` services for showing the same things programmatically. See [Menus](https://meta.discourse.org/styleguide/molecules/menus), [Tooltips](https://meta.discourse.org/styleguide/molecules/tooltips), and [Toasts](https://meta.discourse.org/styleguide/molecules/toasts).
|
||||
|
||||
Reach for a new component in `frontend/discourse/app/components/` when the thing you are building knows about Discourse domain concepts (a topic, a post, a category, a user's notification level). Add to `ui-kit/` only when the primitive is domain-free and useful to more than one consumer; see [Adding a primitive](#adding-a-primitive) below.
|
||||
|
||||
# What is in it
|
||||
|
||||
The groups below are a map, not an exhaustive list. Run `ls frontend/discourse/app/ui-kit` for the current inventory, and read the JSDoc/TSDoc of each file for its arguments and blocks.
|
||||
|
||||
## Actions
|
||||
|
||||
| Primitive | Use it for | Styleguide |
|
||||
| ------------------------------------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
|
||||
| `DButton` | Every clickable action. Handles labels, icons, `@isLoading`, `@disabled`, and ARIA. | [Buttons](https://meta.discourse.org/styleguide/atoms/buttons) |
|
||||
| `DComboButton` | A primary action paired with a dropdown of secondary actions. | [Combo button](https://meta.discourse.org/styleguide/molecules/combo-button) |
|
||||
| `DCopyButton` | Copy a value to the clipboard with feedback. | |
|
||||
| `DPageActionButton` | Actions in a page header. | |
|
||||
| `DBadgeButton`, `DToggleSwitch`, `DTogglePasswordMask` | Toggles and badge-shaped actions. | |
|
||||
| `DShortcut` | Render a keyboard shortcut in the platform's notation. | [Shortcut](https://meta.discourse.org/styleguide/atoms/shortcut) |
|
||||
|
||||
## Inputs
|
||||
|
||||
Form-level composition belongs to [FormKit](22-form-kit.md). These are the underlying controls, useful when a single control is needed outside a form.
|
||||
|
||||
| Primitive | Use it for | Styleguide |
|
||||
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `DSelect` | A native `<select>` with Discourse styling. | [Dropdowns](https://meta.discourse.org/styleguide/atoms/dropdowns) |
|
||||
| `DMultiSelect` | Choose several values with search. | [Multi select](https://meta.discourse.org/styleguide/molecules/multi-select) |
|
||||
| `DTextField`, `DTextarea`, `DExpandingTextArea`, `DPasswordField`, `DRadioButton` | Single controls. | [Forms](https://meta.discourse.org/styleguide/atoms/forms) |
|
||||
| `DDateInput`, `DDatePicker`, `DTimeInput`, `DDateTimeInput`, `DDateTimeInputRange`, `DFutureDateInput`, `DRelativeTimePicker`, `DTimeShortcutPicker`, `DCalendarDateTimeInput` | Dates, times, and ranges. | [Date/time inputs](https://meta.discourse.org/styleguide/atoms/date-time-inputs) |
|
||||
| `DOtp`, `DSecondFactorInput` | One-time codes. | [OTP](https://meta.discourse.org/styleguide/atoms/otp) |
|
||||
| `DFilterInput`, `DFilterControls` | Filter a list. | |
|
||||
| `DColorPicker`, `DIconGridPicker`, `DPickFilesButton` | Specialised pickers. | |
|
||||
| `DCharCounter`, `DInputTip`, `DPopupInputTip` | Feedback attached to an input. | [Char counter](https://meta.discourse.org/styleguide/molecules/char-counter) |
|
||||
| `DAccessControl`, `DAccessControlField` | Edit access control lists. | |
|
||||
| `DEditor` | The composer's markdown editor. | |
|
||||
|
||||
## Page chrome and layout
|
||||
|
||||
| Primitive | Use it for | Styleguide |
|
||||
| ------------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `DPageHeader`, `DPageSubheader` | Titles, breadcrumbs, tabs, and actions of an admin or settings page. | |
|
||||
| `DBreadcrumbsContainer`, `DBreadcrumbsItem` | Breadcrumb trails registered from routes. | [Breadcrumbs](https://meta.discourse.org/styleguide/molecules/bread-crumbs) |
|
||||
| `DNavItem`, `DNavigationItem`, `DHorizontalOverflowNav` | Navigation bars, including ones that scroll horizontally when they overflow. | [Navigation bar](https://meta.discourse.org/styleguide/molecules/navigation-bar) |
|
||||
| `DResponsiveTable`, `DTableHeaderToggle` | Tables that stay usable on narrow viewports, with sortable column headers. | |
|
||||
| `DStatTiles` | Rows of headline numbers. | |
|
||||
| `DTapTileGrid`, `DTapTile` | Grids of tappable choices. | |
|
||||
| `DPostAccordion`, `DPostAccordionItem` | Collapsible sections. | |
|
||||
| `DResizeHandles`, `DResizeSeparator` | User-resizable regions. | [Drag and drop](https://meta.discourse.org/styleguide/molecules/drag-and-drop) |
|
||||
| `DSaveControls` | The save button and "saved" feedback of a settings form. | |
|
||||
|
||||
## Loading, empty, and feedback states
|
||||
|
||||
| Primitive | Use it for |
|
||||
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `DSkeleton` | Placeholder shapes while content loads. Prefer a skeleton that mirrors the eventual layout over a spinner. |
|
||||
| `DConditionalLoadingSpinner`, `DConditionalLoadingSection` | Show a spinner (`@condition`) or a dimmed, labelled section (`@isLoading`) in place of content while it loads. |
|
||||
| `DAsyncContent` | Render content that is loaded asynchronously. Give it a promise, a `TrackedAsyncData`, or a function that fetches the data via `@asyncData`, and fill the `loading`, `content`, `empty`, and `error` blocks; it reloads when `@context` changes, so there is no need to hand-roll loading flags and error handling. |
|
||||
| `DEmptyState` | A titled, illustrated "nothing here" message with an optional call to action. See [Empty state](https://meta.discourse.org/styleguide/molecules/empty-state). |
|
||||
| `DFlashMessage` | Inline success, error, and warning banners. |
|
||||
| `DLoadMore` | Infinite scrolling. |
|
||||
|
||||
## Content rendering
|
||||
|
||||
| Primitive | Use it for |
|
||||
| ---------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| `DCookText` | Render raw markdown as cooked HTML. |
|
||||
| `DDecoratedHtml` | Render HTML and apply the decorators registered through the plugin API to it. |
|
||||
| `DHighlightedCode` | Syntax-highlighted code. |
|
||||
| `DHtmlWithLinks`, `DCustomHtml` | Trusted HTML fragments. |
|
||||
| `DInterpolatedTranslation`, `DCountI18n` | Translations whose placeholders are components or counts. |
|
||||
| `DRelativeDate` | A live-updating relative timestamp. |
|
||||
| `DCdnImg`, `DLightDarkImg` | Images that follow the CDN and the colour scheme. |
|
||||
|
||||
## Users, avatars, and categories
|
||||
|
||||
`DUserAvatar`, `DUserAvatarFlair`, `DAvatarFlair`, `DUserLink`, `DUserInfo`, `DUserStat`, `DUserStatusMessage`, `DSmallUserList`, `DBadgeCard`, plus the `d-avatar`, `d-bound-avatar`, `d-category-badge`, `d-category-link`, `d-discourse-tag`, and `d-topic-link` helpers. See [Categories](https://meta.discourse.org/styleguide/molecules/categories).
|
||||
|
||||
## Overlays
|
||||
|
||||
`DModal` and `DModalCancel` render modals; see the [DModal API](12-dmodal-api.md). `DDropdownMenu` renders the content of a menu opened through the `menu` service. `DConditionalInElement` renders its block into `@element` elsewhere in the DOM, or in place when `@inline` is set.
|
||||
|
||||
## Helpers
|
||||
|
||||
`d-icon`, `d-icon-or-image`, `d-emoji`, `d-replace-emoji`, `d-format-date`, `d-format-duration`, `d-number`, `d-age-with-tooltip`, `d-dasherize`, `d-dir-span`, `d-unique-id`, `d-base-path`, `d-loading-spinner`, `d-element` (a typed wrapper for a chosen tag, so attributes and modifiers are checked against the right element), and `d-concat-class` (join class names, dropping falsy ones).
|
||||
|
||||
## Modifiers
|
||||
|
||||
| Modifier | Use it for |
|
||||
| --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |
|
||||
| `d-auto-focus` | Focus an element when it renders. |
|
||||
| `d-trap-tab` | Keep keyboard focus inside a dialog. |
|
||||
| `d-tab-to-sibling` | Make Tab move focus between sibling elements. |
|
||||
| `d-close-on-click-outside` | Dismiss an overlay when the user clicks elsewhere. |
|
||||
| `d-on-resize`, `d-observe-intersection`, `d-scroll-into-view` | React to layout and visibility. |
|
||||
| `d-autocomplete` | Attach an autocomplete popup to a text input. |
|
||||
| `d-swipe`, `d-pointer-drag`, `d-resize-edge`, `d-drag-dwell`, `d-drag-and-drop-*` | Gestures. See [Drag, resize, and gesture primitives](30-drag-and-gesture-primitives.md). |
|
||||
|
||||
# Adding a primitive
|
||||
|
||||
A primitive belongs in `ui-kit/` when it is **domain-free** (it does not know what a topic or a post is) and either already has two consumers or is an obvious platform capability (a focus trap, a skeleton, a keyboard-shortcut renderer). Otherwise keep it next to its single consumer in `app/components/`.
|
||||
|
||||
When you add one:
|
||||
|
||||
- **Name it `d-<name>`** and put it at `frontend/discourse/app/ui-kit/d-<name>.gts` (or `helpers/d-<name>.js`, `modifiers/d-<name>.ts`). See [Splitting a large primitive](#splitting-a-large-primitive) when one file is not enough.
|
||||
- **Write it in TypeScript with a `Signature`** (`Args`, `Blocks`, `Element`) and TSDoc on the class and every public argument. The kit is platform-level code, so accurate types matter more here than elsewhere; `d-shortcut.gts`, `d-skeleton.gts`, `d-resize-separator.gts`, and `modifiers/d-drag-dwell.ts` are good models. See [Types](27-types.md).
|
||||
- **Forward `...attributes`** to the element consumers will want to target, and declare that element in the `Element` type so the type checker allows attributes and modifiers on it.
|
||||
- **Use BEM class names rooted at the component name** (older primitives predate this rule; new ones must follow it), `d-<name>`, `d-<name>__part`, `d-<name>--modifier`, and put the stylesheet at `app/assets/stylesheets/common/components/d-<name>.scss`, registered in `_index.scss`. Use colour and spacing tokens only; no hard-coded colours. See [CSS guidelines](26-css-guidelines-bem.md).
|
||||
- **Prefer named blocks over boolean arguments** when the consumer supplies content, and yield a small API object when the consumer needs to call back into the component.
|
||||
- **Take strings already translated.** A primitive receives display text as a plain `string` argument (`@label`, `@title`) that the consumer has already passed through `i18n()`; it never resolves translation keys itself. The kit does not know which locale file a consumer's key lives in, and a key-taking argument forces a parallel `@translatedX` argument for every string. `d-resize-separator.gts` documents its `label` this way. `DButton`'s `@label`-as-key plus `@translatedLabel` pair predates this rule; do not copy it.
|
||||
- **Do not mention plugins or specific libraries** in its docs or comments; describe the mechanism instead.
|
||||
|
||||
## Splitting a large primitive
|
||||
|
||||
A primitive whose behaviour outgrows one file keeps **one public entry module** and moves everything else into a sibling directory named after it. Consumers import only the entry; nothing outside the directory imports from `-internals/`.
|
||||
|
||||
```text
|
||||
ui-kit/
|
||||
d-widget.gts # the public component; the only import path
|
||||
d-widget/
|
||||
README.md # what each collaborator owns and why
|
||||
types.ts # every interface, incl. the ones the entry re-exports
|
||||
-internals/
|
||||
constants.ts # values shared by more than one collaborator
|
||||
engine/ # the headless logic: state, algebra, resolution
|
||||
coordinators/ # non-rendering classes the component constructs once
|
||||
parts/ # rendering subcomponents that add no wrapper DOM
|
||||
modifiers/ # element-attached behaviour private to this primitive
|
||||
modifiers/
|
||||
d-gesture.ts # a modifier entry follows the same shape
|
||||
d-gesture/
|
||||
types.ts
|
||||
keyboard.ts, strategies/, …
|
||||
```
|
||||
|
||||
Use the buckets that apply; a two-file split needs only `types.ts`. What goes where:
|
||||
|
||||
- **`types.ts`** holds every interface, including the ones the entry re-exports as public API. It is the documentation home for the argument reference, so the entry's `Args` block can point at it.
|
||||
- **`-internals/engine/`** is the headless layer: state, filtering, the move or selection algebra, value resolution. It has no DOM and no component reference, which is what makes it unit-testable on its own.
|
||||
- **`-internals/coordinators/`** are plain classes the component constructs once and configures downward with thunks (`() => this.args.x`); they never hold a reference back to the component. Announcers, menu coordinators, and load-feedback timers live here.
|
||||
- **`-internals/parts/`** are the rendering subcomponents. They receive the stable helper objects (the engine, a presenter) plus per-slot inputs, and add no wrapper DOM of their own.
|
||||
- **`-internals/constants.ts`** exists so two collaborators that must agree on a value (a menu identifier, a selector) import it rather than restating it.
|
||||
- **`README.md`** explains the split: what each collaborator owns, the invariants that cross files, and the design decisions a future reader would otherwise undo.
|
||||
|
||||
Something shared by several primitives, rather than private to one, goes in `ui-kit/-internals/<topic>/` (cursor navigation, for example), still off-limits to consumers.
|
||||
|
||||
# Blast radius and backward compatibility
|
||||
|
||||
The kit exists to build all of Discourse's UI: core, every plugin, and every theme render through it. A change here therefore has a far larger blast radius than a change to a single feature, and code you cannot see (third-party plugins and themes) depends on the public surface exactly as it is today.
|
||||
|
||||
Before changing anything public facing (an argument, a block, a yielded API object, a class name, DOM structure, or a helper's or modifier's signature), work out:
|
||||
|
||||
- **Who can be relying on it.** Search core, the in-repo plugins, and the wider plugin and theme ecosystem for the argument or class name; assume any exported name or documented argument has consumers you cannot see.
|
||||
- **Whether the change is backward compatible.** Adding an optional argument or block is; renaming, removing, changing a default, changing the element `...attributes` land on, or restructuring the DOM that stylesheets target is not.
|
||||
- **Whether a deprecation cycle is required.** A breaking change to a public surface ships behind a deprecation first: keep the old path working, emit a deprecation with an id and a `since` version, document the replacement, and remove it only after the cycle. Do not skip this because the old behaviour looks unused.
|
||||
|
||||
For anything **new** (a feature, an argument, a component, a modifier), the bar is exhaustive testing rather than a happy-path check: every argument and block, keyboard and pointer input, the states a consumer can put it in, error paths, and the accessibility contract. A primitive that ships with a gap ships that gap to every surface that adopts it.
|
||||
|
||||
# Testing a primitive
|
||||
|
||||
- **Rendering tests** go in `frontend/discourse/tests/integration/ui-kit/d-<name>-test.gjs`, next to the other 40-odd kit tests. Assert through the public API and observable DOM, not internal state. Cover every argument and block, every input method the primitive answers to, and the states it can be driven into; the drag-and-drop family in `frontend/discourse/tests/integration/ui-kit/modifiers/` shows the expected depth (modifier tests live in that `modifiers/` subdirectory, helper tests in `helpers/`).
|
||||
- **Type tests** for `.gts` primitives go in `frontend/discourse/type-tests/ui-kit/d-<name>-test.gts`, using `@glint-expect-error` (and `expect-type` where a value type matters) to pin down both what compiles and what must not.
|
||||
- **Module-level state** (registries, callbacks) needs a reset export that `frontend/discourse/tests/helpers/qunit-helpers.js` calls from `testCleanup()`, as `d-decorated-html` and `d-editor` do.
|
||||
- **System specs** for behaviour a component test cannot reach (real drag negotiation, portaled overlays, scroll containers) go in the styleguide plugin when the primitive has no core consumer to exercise it.
|
||||
|
||||
# Styleguide section
|
||||
|
||||
Every primitive is browsable at `/styleguide/<category>/<section>`, and the section is part of the deliverable, not an afterthought. Add or extend one under `plugins/styleguide/assets/javascripts/discourse/components/sections/` and register it in `plugins/styleguide/assets/javascripts/discourse/lib/styleguide.js`.
|
||||
|
||||
A section should be designed, with multiple examples that each isolate one capability. `sections/molecules/drag-and-drop.gjs` is the model: it groups its examples with `StyleguideGroups`, and each `Example` carries a title, a `@kind`, a description of what the example demonstrates, a `@tryThis` prompt telling the reader what to do, an optional `@note` on the subtlety it exposes, and the source via `@code` so the reader can copy it. Aim for one example per argument or behaviour worth understanding (types, positions, disabled state, nesting, custom preview, and so on), rather than a single kitchen-sink demo.
|
||||
|
||||
# Related guides
|
||||
|
||||
- [FormKit](22-form-kit.md) for forms.
|
||||
- [DModal API](12-dmodal-api.md) for modals.
|
||||
- [CSS guidelines](26-css-guidelines-bem.md) for BEM and tokens.
|
||||
- [Designing for devices](28-designing-for-devices.md) and [responsive widths](29-designing-for-responsive-widths.md).
|
||||
- [Drag, resize, and gesture primitives](30-drag-and-gesture-primitives.md) for the modifiers that handle input.
|
||||
- [Types](27-types.md) for the Glint and TypeScript conventions the kit follows.
|
||||
+1
-1
@@ -4,7 +4,7 @@ short_title: DModal API
|
||||
id: dmodal-api
|
||||
---
|
||||
|
||||
Discourse 3.1.0.beta6 ships with a brand new `<DModal>` component-based API.
|
||||
Discourse 3.1.0.beta6 ships with a brand new `<DModal>` component-based API. `DModal` is part of the [UI kit](02-ui-kit.md) and is imported from `discourse/ui-kit/d-modal`.
|
||||
|
||||
> :information_source: This supersedes the old controller-based API, which is now deprecated. If you have existing modals using the old APIs, check out the migration guide [here](https://meta.discourse.org/t/converting-modals-from-legacy-controllers-to-new-dmodal-component-api/268057).
|
||||
|
||||
+3
-3
@@ -16,7 +16,7 @@ General guidance on CSS customization can be found in [the Designer's Guide](htt
|
||||
|
||||
On desktop, the topic list is rendered as a table. On mobile, each topic row uses a separate single-cell layout, so `topic-list-columns` only affects the desktop table.
|
||||
|
||||
The [Plugin API](12-pluginapi.md) can be used to customize the desktop columns via the `topic-list-columns` [Value Transformer](23-transformers.md). This is the most surgical way to add, remove, replace, or reorder columns without taking ownership of the full row markup.
|
||||
The [Plugin API](13-pluginapi.md) can be used to customize the desktop columns via the `topic-list-columns` [Value Transformer](24-transformers.md). This is the most surgical way to add, remove, replace, or reorder columns without taking ownership of the full row markup.
|
||||
|
||||
`topic-list-columns` is a **mutable** transformer. Core builds a `DAG` of columns, passes that mutable object through registered transformers, and resolves it afterwards. In practice, that means you should mutate the `columns` object directly.
|
||||
|
||||
@@ -123,7 +123,7 @@ export default apiInitializer((api) => {
|
||||
|
||||
## Introducing content via Plugin Outlets
|
||||
|
||||
Prefer [Plugin Outlets](13-plugin-outlet-connectors.md) when you want to inject content without taking ownership of the whole row structure.
|
||||
Prefer [Plugin Outlets](14-plugin-outlet-connectors.md) when you want to inject content without taking ownership of the whole row structure.
|
||||
|
||||
Some of the most useful topic-list outlets are:
|
||||
|
||||
@@ -150,7 +150,7 @@ If using this strategy, you should take extra care to ensure that your code is w
|
||||
|
||||
## Other tweaks via Transformers and Theme Modifiers
|
||||
|
||||
A number of [Transformers](23-transformers.md) allow small, targeted changes to the topic-list implementation:
|
||||
A number of [Transformers](24-transformers.md) allow small, targeted changes to the topic-list implementation:
|
||||
|
||||
- **`topic-list-columns`** (mutable context: `{ listContext, category, filter }`) - mutate the desktop column DAG
|
||||
- **`topic-list-class`** (context: `{ topics, listContext }`) - return classes to add to the topic list `<table>`
|
||||
+1
-1
@@ -65,4 +65,4 @@ A great real world example of our CSS classes in use in Discourse is within the
|
||||
|
||||
## See also
|
||||
|
||||
- [Designing for Different Devices](27-designing-for-devices.md) — adapting styles to viewport size, touch vs. hover, and other device characteristics.
|
||||
- [Designing for Different Devices](28-designing-for-devices.md) — adapting styles to viewport size, touch vs. hover, and other device characteristics.
|
||||
+3
-3
@@ -52,9 +52,9 @@ class MyComponent extends Component {
|
||||
|
||||
Historically, Discourse shipped two completely different layouts and stylesheets for "mobile" and "desktop" views, based on the browser's user-agent. Developers would target these modes by putting CSS in specific mobile/desktop directories, by using the `.mobile-view`/`.desktop-view` HTML classes, and the `site.mobileView` boolean in JavaScript.
|
||||
|
||||
These techniques are now considered deprecated and should be replaced with the [viewport and capability-based strategies](28-designing-for-responsive-widths.md) discussed in the next document. For backwards-compatibility, legacy desktop/mobile CSS is used when the viewport is larger/smaller than the `sm` threshold.
|
||||
These techniques are now considered deprecated and should be replaced with the [viewport and capability-based strategies](29-designing-for-responsive-widths.md) discussed in the next document. For backwards-compatibility, legacy desktop/mobile CSS is used when the viewport is larger/smaller than the `sm` threshold.
|
||||
|
||||
## See also
|
||||
|
||||
- [Designing for Responsive Widths](28-designing-for-responsive-widths.md) — breakpoints, viewport size, and container queries.
|
||||
- [Guidelines for CSS classes using BEM](25-css-guidelines-bem.md) — CSS class naming conventions.
|
||||
- [Designing for Responsive Widths](29-designing-for-responsive-widths.md) — breakpoints, viewport size, and container queries.
|
||||
- [Guidelines for CSS classes using BEM](26-css-guidelines-bem.md) — CSS class naming conventions.
|
||||
+2
-2
@@ -102,5 +102,5 @@ For a real example, see `.db-whos-posting` in `app/assets/stylesheets/admin/dash
|
||||
|
||||
## See also
|
||||
|
||||
- [Designing for Different Devices (Touch & Hover)](27-designing-for-devices.md) — detecting touch/hover capability, and legacy mobile/desktop modes.
|
||||
- [Guidelines for CSS classes using BEM](25-css-guidelines-bem.md) — CSS class naming conventions.
|
||||
- [Designing for Different Devices (Touch & Hover)](28-designing-for-devices.md) — detecting touch/hover capability, and legacy mobile/desktop modes.
|
||||
- [Guidelines for CSS classes using BEM](26-css-guidelines-bem.md) — CSS class naming conventions.
|
||||
@@ -26,7 +26,7 @@ If you're stuck on something that relates to themes, feel free to create a post
|
||||
|
||||
- https://meta.discourse.org/t/adding-localizable-strings-to-themes-and-theme-components/109867
|
||||
|
||||
- [Automatically lint and format code before commits](../03-code-internals/02-lint-and-format.md)
|
||||
- [Automatically lint and format code before commits](../03-code-internals/03-lint-and-format.md)
|
||||
|
||||
- https://meta.discourse.org/t/write-end-to-end-system-specs-for-the-discourse-user-interface/325937
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ export interface DragDwell<Target> {
|
||||
* `Target` must be non-nullish: `null` is reserved as the clear sentinel.
|
||||
*
|
||||
* Guide to choosing between the drag and gesture primitives:
|
||||
* `docs/developer-guides/docs/03-code-internals/29-drag-and-gesture-primitives.md`
|
||||
* `docs/developer-guides/docs/03-code-internals/30-drag-and-gesture-primitives.md`
|
||||
*
|
||||
* @see The `dDragDwell` modifier, which packages this for the common case of
|
||||
* one element observed through the drag monitors.
|
||||
|
||||
@@ -112,7 +112,7 @@ interface DSwipeSignature {
|
||||
* ```
|
||||
*
|
||||
* Guide to choosing between the gesture primitives:
|
||||
* `docs/developer-guides/docs/03-code-internals/29-drag-and-gesture-primitives.md`
|
||||
* `docs/developer-guides/docs/03-code-internals/30-drag-and-gesture-primitives.md`
|
||||
*
|
||||
* @see The `dPointerDrag` modifier for a value that tracks the pointer continuously. This
|
||||
* reports a discrete directional flick, not a continuous transform.
|
||||
|
||||
Reference in New Issue
Block a user