@fusion.dev/helix-docs 0.0.0-dev

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/LICENSE +21 -0
  2. package/content/accessibility.mdx +117 -0
  3. package/content/angular.mdx +60 -0
  4. package/content/browser-support.mdx +83 -0
  5. package/content/building-on-the-headless-tier.mdx +143 -0
  6. package/content/charting.mdx +93 -0
  7. package/content/coding-agents.mdx +139 -0
  8. package/content/color-modes.mdx +87 -0
  9. package/content/color.mdx +92 -0
  10. package/content/composition.mdx +171 -0
  11. package/content/elevation-and-material.mdx +93 -0
  12. package/content/events.mdx +125 -0
  13. package/content/forms.mdx +170 -0
  14. package/content/iconography.mdx +80 -0
  15. package/content/installation.mdx +125 -0
  16. package/content/intent-reference.mdx +107 -0
  17. package/content/introduction.mdx +69 -0
  18. package/content/localization.mdx +95 -0
  19. package/content/metrics.mdx +399 -0
  20. package/content/motion.mdx +91 -0
  21. package/content/other-frameworks.mdx +70 -0
  22. package/content/overlays.mdx +102 -0
  23. package/content/overriding-a-component.mdx +160 -0
  24. package/content/react.mdx +144 -0
  25. package/content/recipes.mdx +131 -0
  26. package/content/routing.mdx +208 -0
  27. package/content/sandbox.mdx +76 -0
  28. package/content/server-rendering.mdx +103 -0
  29. package/content/shape-and-borders.mdx +80 -0
  30. package/content/size-and-density.mdx +86 -0
  31. package/content/space-and-size.mdx +89 -0
  32. package/content/state-and-control.mdx +132 -0
  33. package/content/surfaces-and-separation.mdx +99 -0
  34. package/content/svelte.mdx +45 -0
  35. package/content/theme-builder.mdx +66 -0
  36. package/content/theming.mdx +111 -0
  37. package/content/tiers.mdx +159 -0
  38. package/content/tokens.mdx +88 -0
  39. package/content/typography.mdx +117 -0
  40. package/content/using-helix-with-an-agent.mdx +231 -0
  41. package/content/using-the-elements.mdx +125 -0
  42. package/content/versioning.mdx +72 -0
  43. package/content/visual-axes.mdx +75 -0
  44. package/content/vue.mdx +80 -0
  45. package/content/without-a-bundler.mdx +119 -0
  46. package/dist/catalog.d.ts +186 -0
  47. package/dist/catalog.js +296 -0
  48. package/dist/index.d.ts +7 -0
  49. package/dist/index.js +6 -0
  50. package/dist/naming.d.ts +14 -0
  51. package/dist/naming.js +28 -0
  52. package/dist/pages.generated.d.ts +7 -0
  53. package/dist/pages.generated.js +284 -0
  54. package/dist/reference.d.ts +105 -0
  55. package/dist/reference.js +181 -0
  56. package/dist/registry.d.ts +89 -0
  57. package/dist/registry.js +70 -0
  58. package/dist/sections.d.ts +44 -0
  59. package/dist/sections.js +36 -0
  60. package/dist/sources.generated.d.ts +6 -0
  61. package/dist/sources.generated.js +5158 -0
  62. package/package.json +46 -0
@@ -0,0 +1,87 @@
1
+ ---
2
+ description: One attribute, two modes, and no token restated to honor it.
3
+ section: Foundations
4
+ title: Color modes
5
+ ---
6
+
7
+ # Color modes
8
+
9
+ Light and dark are one attribute and no duplicated tokens. There is no second
10
+ stylesheet, no `.dark` class to toggle, and nothing to remember to update twice.
11
+
12
+ ## How it works
13
+
14
+ ```css
15
+ :root {
16
+ color-scheme: light dark;
17
+ --helix-color-background: light-dark(#fff, #0b0b0d);
18
+ }
19
+ ```
20
+
21
+ Every token is written once, with both halves in a `light-dark()`. Which half
22
+ resolves is decided by `color-scheme` and nothing else, so the mode is a single
23
+ inherited property and no token is restated to honor it. That is 761
24
+ declarations across the library, 135 of them in the token layer, expressing two
25
+ modes without a second copy of anything.
26
+
27
+ ## Setting the mode
28
+
29
+ ```html
30
+ <html data-color-mode="dark">
31
+ ```
32
+
33
+ `[data-color-mode="light"]` and `[data-color-mode="dark"]` set `color-scheme`,
34
+ and `color-scheme` inherits, so the attribute works **on any element** rather
35
+ than only on the root:
36
+
37
+ ```html
38
+ <div data-color-mode="light">
39
+ <!-- A light preview inside a dark page. One attribute. -->
40
+ </div>
41
+ ```
42
+
43
+ That is the whole mechanism, and it is why the theme builder can show you a
44
+ light and a dark rendering of the same component side by side without rendering
45
+ two documents.
46
+
47
+ ## Following the system
48
+
49
+ Unset, `:root` declares `color-scheme: light dark`, which means the operating
50
+ system decides. That is the default and it is usually right.
51
+
52
+ A `prefers-color-scheme` media query still exists in the token layer for one
53
+ narrow case rather than being the mechanism: a value that has to be *derived*
54
+ from the mode, rather than chosen between two, cannot be expressed as
55
+ `light-dark()` and has to ask which mode is current.
56
+
57
+ ## The one component that inverts
58
+
59
+ `Toast` renders in the opposite mode to the page it is on, which is why it is
60
+ the exception written into the base layer:
61
+
62
+ ```css
63
+ helix-toast:not([data-color-mode]) {
64
+ color-scheme: var(--_helix-mode-inverted);
65
+ }
66
+ ```
67
+
68
+ A toast is chrome over the page rather than part of it, and inverting is how it
69
+ reads as separate without a border. Opt out by putting `data-color-mode` on it,
70
+ which then wins, because the rule only applies to a toast that has not been
71
+ told.
72
+
73
+ ## What this means for your own CSS
74
+
75
+ Write `light-dark()` for your own colours and set nothing else:
76
+
77
+ ```css
78
+ .callout {
79
+ background: light-dark(#eef, #223);
80
+ }
81
+ ```
82
+
83
+ It will follow the page, follow a subtree that was told otherwise, and follow
84
+ the system when nothing has said. Do not write a `prefers-color-scheme` query
85
+ for it, and do not key off the `data-color-mode` attribute: both stop working
86
+ the moment a subtree overrides the mode, which is the case this design exists to
87
+ make cheap.
@@ -0,0 +1,92 @@
1
+ ---
2
+ description: The roles, the ramps, and why the palette is solved rather than picked.
3
+ section: Foundations
4
+ title: Color
5
+ ---
6
+
7
+ # Color
8
+
9
+ Seventy-four of the library's 177 tokens are color, which is most of what a
10
+ theme is. They are roles rather than values: nothing in Helix is named after
11
+ what it looks like, because a name like `--brand-blue-500` stops being true the
12
+ moment anybody rethemes.
13
+
14
+ ## The roles
15
+
16
+ **Grounds**, which things sit on. `background` is the page itself, and the
17
+ surfaces above it are a ladder rather than a set:
18
+
19
+ [//]: # (tokens:surface-ladder)
20
+
21
+ | Token | Sample | For |
22
+ | --- | --- | --- |
23
+ | `--helix-color-background` | <span data-token-swatch style={{ background: 'var(--helix-color-background)' }} /> | The page. Told apart by nothing, because it is the ground. |
24
+ | `--helix-color-surface` | <span data-token-swatch style={{ background: 'var(--helix-color-surface)' }} /> | A plane on the page: a card, or a panel you own. Told apart by a fill, an edge or a lift, whichever the policy leads with. |
25
+ | `--helix-color-surface-raised` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-raised)' }} /> | Lifted while still lying on the page: a selected tab, a hovered row. Told apart by its fill, which goes down the ramp rather than up. |
26
+ | `--helix-color-surface-overlay` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-overlay)' }} /> | Over the page rather than on it: a menu, a popover, a dialog. Told apart by its shadow and by what it covers. |
27
+ | `--helix-color-surface-sunken` | <span data-token-swatch style={{ background: 'var(--helix-color-surface-sunken)' }} /> | A well a value goes into: an input, a track, a groove. Told apart by its fill, inward. |
28
+
29
+ [//]: # (/tokens)
30
+
31
+ Which one to reach for is a question about the geometry rather than about the
32
+ shade, and it is the thing people most often get wrong. See [Surfaces and
33
+ separation](./surfaces-and-separation).
34
+
35
+ **Inks**, which go on grounds. `foreground`, then `foreground-muted` and
36
+ `foreground-subtle` for text that recedes, and `foreground-on-accent` for text
37
+ that sits on a filled accent rather than on a surface.
38
+
39
+ **Intent**, which says what something means. `primary` and `accent` for
40
+ emphasis, each with `-hover`, `-active` and `-subtle`; `info`, `success`,
41
+ `warning` and `danger` for status, each with a `-surface` and a `-surface-hover`
42
+ for the tinted box the message sits in.
43
+
44
+ **Series**, twenty-four of them, for charts. Ordered rather than arbitrary: a
45
+ chart with three lines uses the first three, and they are chosen to stay
46
+ distinguishable in that order rather than to look good as a full set.
47
+
48
+ **Syntax**, eight roles for code: `callable`, `comment`, `constant`, `invalid`,
49
+ `keyword`, `name`, `string`, `tag`. Deliberately fewer than a full editor theme,
50
+ because a code block in documentation is read rather than edited.
51
+
52
+ ## OKLCH
53
+
54
+ Every color is authored in OKLCH. Equal numeric steps in sRGB are not equal
55
+ perceptual steps, which makes both ramp generation and contrast arithmetic
56
+ unreliable: a ramp that is even by the numbers looks uneven, and a contrast
57
+ solver working in the wrong space cannot converge on a target it can measure.
58
+
59
+ Concretely, this is what lets the theme engine take a brand color and *solve*
60
+ for a palette rather than have someone pick one. See [Deriving a
61
+ theme](./theming).
62
+
63
+ ## Contrast is a guarantee
64
+
65
+ The engine measures every ink against the ground it will really be painted on
66
+ and searches for the lightness that meets the target, rather than proposing a
67
+ value and checking it afterwards. Every pairing it produces meets the target by
68
+ construction.
69
+
70
+ That guarantee is only as good as the list of pairs somebody thought of, so two
71
+ things are checked rather than one. The engine's declared surface model is held
72
+ against what components actually paint, and then every pairing is measured on a
73
+ laid-out page, which is the only way to see the ones no stylesheet states: just
74
+ eleven rules in the library set an ink and a ground together, where 165 set only
75
+ an ink and 112 only a ground, and a pairing the cascade assembled is still a
76
+ pairing a reader has to be able to read.
77
+
78
+ ## Do not reach past the semantic layer
79
+
80
+ ```css
81
+ /* Yours to read. */
82
+ color: var(--helix-color-foreground-muted);
83
+
84
+ /* Not yours to read, and `check:tokens` fails the build on it. */
85
+ color: var(--_helix-neutral-500);
86
+ ```
87
+
88
+ The primitive layer underneath, `--_helix-neutral-500` and its kind, is
89
+ `helix/styles`'s own business. The underscore marks it private and the
90
+ indirection is the whole point: it is what lets a theme change what
91
+ `--helix-color-primary` resolves to without every component knowing which step
92
+ of which ramp it happened to be.
@@ -0,0 +1,171 @@
1
+ ---
2
+ description: Families, parts and groups, and which to reach for.
3
+ section: Essentials
4
+ title: Composition
5
+ ---
6
+
7
+ # Composition
8
+
9
+ Helix has families, parts and groups, and they are not stylistic choices. Each
10
+ answers a different question, and knowing which you are looking at tells you
11
+ what to import and what to write.
12
+
13
+ ## Namespaces mean composition
14
+
15
+ `Tabs.Root` contains `Tabs.Tab`. The namespace says *this goes inside that*.
16
+
17
+ It never says *this is a version of that*. An icon-only button is not a part of
18
+ `Button`, so `IconButton` is top-level. The rule falls out of one line:
19
+
20
+ > Variants are props. Alternatives are separate components.
21
+
22
+ A button that is filled instead of outlined is the same component with a
23
+ different `emphasis`. A button that holds only an icon has a different
24
+ accessible-name requirement and a different shape, so it is its own component.
25
+
26
+ ## When a region becomes an element
27
+
28
+ When it has behavior, state or API. Otherwise it is a slot.
29
+
30
+ `Card`'s header, body and footer carried none of the three, so five elements
31
+ became one and a title became a real `<h2>` the consumer writes. `Tabs.Tab`,
32
+ `Select.Option` and `Accordion.Item` do carry state, so they stay elements.
33
+
34
+ **An empty region still costs layout.** A `<slot>` is `display: contents`, so an unassigned one generates no
35
+ box: no grid row, no flex item, no gap. A wrapper generates one either way.
36
+ Measured on Card with identical content: wrappers present but empty rendered
37
+ 85px against an ideal of 53px. Bare slots rendered 53px with no hiding logic at
38
+ all.
39
+
40
+ ## When a group is a part
41
+
42
+ The test is whether the group reports a form value.
43
+
44
+ **`Radio.Group` and `Toggle.Group` do.** They own the `name`, the `value` and
45
+ the validity, and a lone radio is not a control at all: it is an answer to one.
46
+ The group is what the family is *about*, so it is a part of it, and the member
47
+ has nowhere else to be.
48
+
49
+ **`Checkbox Group` and `Button Group` do not.** Neither calls `setFormValue`.
50
+ Each checkbox is form-associated and submits itself; the group adds
51
+ `role="group"`, a shared name, and one place to read the set. A button needs no
52
+ group at all. Both are their own families, because their members are complete
53
+ controls that the family does not need the group to explain.
54
+
55
+ A container holding mixed content is a family for the same reason. `Toolbar`
56
+ holds buttons, dropdowns and inputs, and is about none of them.
57
+
58
+ ## A content property is its slot's fallback
59
+
60
+ Where a property carries content rather than configuration, it is the fallback
61
+ content of a slot of the same name, never a second thing rendered beside it.
62
+
63
+ ```html
64
+ <!-- The property -->
65
+ <helix-tooltip content="Save"></helix-tooltip>
66
+
67
+ <!-- The slot, which replaces it -->
68
+ <helix-tooltip>
69
+ <strong>Save</strong>, or press <helix-kbd>⌘S</helix-kbd>
70
+ </helix-tooltip>
71
+ ```
72
+
73
+ Text stays an attribute, because that is what most tooltips and most avatars
74
+ hold. Anything richer replaces it by being slotted. Both can never show at once,
75
+ and no component has to detect which you did.
76
+
77
+ One trap: **a whitespace-only text node is assigned to the default slot and
78
+ defeats its fallback.** A newline between the tags is enough. That is
79
+ why some examples close on the same line they open.
80
+
81
+ ## Width comes from the layout
82
+
83
+ There is no `width` or `fill` property on a button, for the same reason there is
84
+ no `size` one: how wide a control is says something about the region it is in.
85
+
86
+ A button already stretches in a column flex or a grid, because that is what a
87
+ stretch-aligned parent does to its children:
88
+
89
+ ```html
90
+ <helix-flex direction="column">
91
+ <helix-button
92
+ emphasis="solid"
93
+ tone="primary"
94
+ >Fills its column</helix-button>
95
+ </helix-flex>
96
+ ```
97
+
98
+ To make one hug its label instead, tell the layout:
99
+
100
+ ```css
101
+ .hug {
102
+ align-self: start;
103
+ }
104
+ ```
105
+
106
+ ```html
107
+ <helix-button class="hug">Hugs its label</helix-button>
108
+ ```
109
+
110
+ The default surprises people: dropping a button into a column makes it full
111
+ width, and the instinct is to look for a property that turns that off.
112
+
113
+ **A fixed width is a different question, and it belongs to your site.** A
114
+ marketing page whose hero buttons are all the same width is a real and reusable
115
+ practice, but the number is a fact about that page's design, not about buttons.
116
+ Name it once where your other page constants live:
117
+
118
+ ```css
119
+ :root {
120
+ --site-cta-inline-size: 13rem;
121
+ }
122
+
123
+ .heroActions helix-button {
124
+ min-inline-size: var(--site-cta-inline-size);
125
+ }
126
+ ```
127
+
128
+ The same reasoning puts a hero's type size outside the component scale: a
129
+ marketing hero is not a component, so its measurements are the site's own.
130
+
131
+ ## One property per intent
132
+
133
+ Every field control names itself the same way, and there are exactly two cases.
134
+
135
+ ```html
136
+ <helix-input label="Files"></helix-input> <!-- a visible label -->
137
+ <helix-input aria-label="Files"></helix-input> <!-- named, nothing drawn -->
138
+ <helix-input></helix-input> <!-- unnamed. avoid -->
139
+ ```
140
+
141
+ `label` means a label. `aria-label` means a name. Neither means a control a
142
+ screen reader announces as "edit text", which is almost never what you want.
143
+ If the page already makes the purpose plain, that is the `aria-label` case, not
144
+ the empty case.
145
+
146
+ **`aria-label` works here because the component forwards it**, which is not true
147
+ of custom elements in general. A custom element
148
+ cannot delegate a label to the control inside its shadow root: an `aria-label`
149
+ you write on the host sits on a wrapper and never reaches the `<input>` that
150
+ actually takes focus. Measured on this library before it was fixed, the inner
151
+ input came back with no label and no name at all.
152
+
153
+ So the field renders the label and clips it, and the name arrives through the
154
+ same `<label for>` a visible one does.
155
+
156
+ There used to be a `hide-label` boolean beside `label`. It is gone: one
157
+ attribute supplying a label and a second saying not to show it is the absence of
158
+ the first, spelled twice.
159
+
160
+ ## Shared behavior becomes its own family
161
+
162
+ `Option`, `Listbox` and `Select` are three families rather than one because the
163
+ keyboard, the filtering, the multi-select rules and the active-item tracking
164
+ should be written once. `Select` renders a `Listbox` internally; a future
165
+ combobox will; a command palette would.
166
+
167
+ The same reasoning made `DrilldownController`: `Menu` had held a page stack and
168
+ derived its rows since it was written, and none of that was about menus. `Menu`
169
+ and `Drilldown` share it now.
170
+
171
+ If you are about to write the second copy of something, that is the signal.
@@ -0,0 +1,93 @@
1
+ ---
2
+ description: Five layers, four shadows, and the glass the material tokens make possible.
3
+ section: Foundations
4
+ title: Elevation and material
5
+ ---
6
+
7
+ # Elevation and material
8
+
9
+ ## Layers
10
+
11
+ [//]: # (tokens:layer)
12
+
13
+ | Token | Value |
14
+ | --- | --- |
15
+ | `--helix-layer-beneath` | `-1` |
16
+ | `--helix-layer-base` | `0` |
17
+ | `--helix-layer-raised` | `10` |
18
+ | `--helix-layer-sticky` | `100` |
19
+ | `--helix-layer-overlay` | `1000` |
20
+ | `--helix-layer-toast` | `1200` |
21
+
22
+ [//]: # (/tokens)
23
+
24
+ Five, named after what lives at each rather than numbered. A numbered scale
25
+ invites `z-index: 1001`, and the number nobody can justify is how stacking
26
+ context bugs start.
27
+
28
+ | Layer | What is at it |
29
+ |---|---|
30
+ | `base` | The document |
31
+ | `raised` | Something lifted but still in the flow: a hovered row, a dragged card |
32
+ | `sticky` | A header or a column that stays while content scrolls under it |
33
+ | `overlay` | Menus, dialogs, popovers, tooltips |
34
+ | `toast` | Above everything, including a modal |
35
+
36
+ **Most overlays do not need these**, because they use the popover API and render
37
+ in the browser's top layer, which is above every z-index by construction. The
38
+ tokens are for the cases the top layer does not cover: your own sticky header,
39
+ your own dragged element, and anything that has to sit between two of ours.
40
+
41
+ ## Shadows
42
+
43
+ [//]: # (tokens:shadow)
44
+
45
+ | Token | Value |
46
+ | --- | --- |
47
+ | `--helix-shadow-none` | `none` |
48
+ | `--helix-shadow-sm` | `0 1px 2px var(--_helix-shadow-ink-sm)` |
49
+ | `--helix-shadow-md` | `0 4px 12px var(--_helix-shadow-ink-md)` |
50
+ | `--helix-shadow-lg` | `0 12px 32px var(--_helix-shadow-ink-lg)` |
51
+ | `--helix-shadow-surface` | `var(--helix-shadow-none)` |
52
+ | `--helix-shadow-floating` | `var(--helix-shadow-lg)` |
53
+
54
+ [//]: # (/tokens)
55
+
56
+ The first four are the values. The last two are the *decision*, and they are
57
+ what a theme sets to make the system shadow-first or border-first. See [Surfaces
58
+ and separation](./surfaces-and-separation).
59
+
60
+ ## Material
61
+
62
+ Eight tokens describing translucency rather than color:
63
+
64
+ [//]: # (tokens:material)
65
+
66
+ | Token | Value | For |
67
+ | --- | --- | --- |
68
+ | `--helix-material-backdrop` | `blur(var(--helix-material-blur-md)) saturate(1.5)` | The wash behind a modal. |
69
+ | `--helix-material-opacity` | `1` | How opaque a translucent surface is. |
70
+ | `--helix-material-blur-sm` | `8px` | The lightest backdrop blur. |
71
+ | `--helix-material-blur-md` | `16px` | The backdrop blur a modal takes. |
72
+ | `--helix-material-blur-lg` | `32px` | The heaviest backdrop blur. |
73
+ | `--helix-material-translucent` | `color-mix(in oklab, light-dark(oklch(1 0 0), oklch(0.21 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258))) 30%, transparent)` | The translucent ground, for a surface that admits what is behind it. |
74
+ | `--helix-material-translucent-strong` | `color-mix(in oklab, light-dark(oklch(1 0 0), oklch(0.21 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258))) 88%, transparent)` | The heavier translucent ground, for one that admits less. |
75
+ | `--helix-material-border` | `light-dark(color-mix(in oklab, oklch(0.15 var(--helix-seed-neutral-chroma, 0) var(--helix-seed-neutral-hue, 258)) 14%, transparent), color-mix(in oklab, oklch(1 0 0) 12%, transparent))` | The edge a translucent surface needs to have any at all. |
76
+
77
+ [//]: # (/tokens)
78
+
79
+ These are what the `material` axis renders: `frosted`, `glass` and `transparent`
80
+ are three points of one optical language, and they are an axis of their own
81
+ rather than values of `emphasis`, because a panel can be glass *and* outlined
82
+ and two things that can be true at once are two axes. See [Visual
83
+ axes](./visual-axes).
84
+
85
+ **Blur is expensive and is the one thing here worth being deliberate about.** A
86
+ `backdrop-filter` forces the compositor to re-read what is behind the element on
87
+ every frame it moves. A frosted header that is always on screen pays that
88
+ continuously. Reach for `translucent` without the blur when the effect is about
89
+ depth rather than about the texture.
90
+
91
+ **`material-border` is not decoration.** A translucent surface takes its color
92
+ from what is behind it, so on a busy background it has no edge at all. The
93
+ border token is what keeps it a shape.
@@ -0,0 +1,125 @@
1
+ ---
2
+ description: The request and the notification, why one name is prefixed and the other is not.
3
+ section: Essentials
4
+ title: Events
5
+ ---
6
+
7
+ # Events
8
+
9
+ **Every state change is announced twice, and the two are for different people.**
10
+
11
+ ```ts
12
+ // The request. Cancelable, carries what is being proposed, and fires before
13
+ // anything moves. This is how you take the decision.
14
+ element.addEventListener('helix-before-change', (event) => {
15
+ if (locked) {
16
+ event.preventDefault()
17
+ }
18
+ })
19
+
20
+ // The notification, under the platform's own name. Fires after the value moved,
21
+ // so it is on the target where every handler ever written expects it.
22
+ element.addEventListener('change', (event) => {
23
+ save(event.target.value)
24
+ })
25
+ ```
26
+
27
+ Most consumers want the second. Bind `onChange` in React or `onchange=` in HTML
28
+ and read the target, exactly as you would from an `<input>`. Reach for the
29
+ prefixed one when you mean to refuse.
30
+
31
+ The class exists rather than a bare `CustomEvent` for three reasons, all about
32
+ the type reaching you: the class name is in the manifest, which is what lets the
33
+ framework typings emit a typed callback instead of `(detail: unknown)`; the
34
+ package augments `HTMLElementEventMap`, which types `addEventListener` for
35
+ people writing plain HTML; and the name lives on the class, so a dispatch and a
36
+ listener cannot disagree about the string.
37
+
38
+ ## Why the request is prefixed and the notification is not
39
+
40
+ **Retargeting.** An event dispatched inside a shadow root is retargeted to look
41
+ like it came from the host, so a bare `input` from a `<helix-input>` is
42
+ indistinguishable from the inner control's own. Prefixed, it cannot be mistaken
43
+ for either.
44
+
45
+ **React's claimed verbs.** React's synthetic system owns `change`, `input`,
46
+ `submit`, `copy`, `focus` and the rest, and a prop matching one of those is
47
+ wired to the native event instead. `onhelix-before-change` matches none of them
48
+ and reaches us, which is exactly what a request needs.
49
+
50
+ **And the notification wants the opposite.** It is not prefixed precisely
51
+ because React claims the verb: `onChange` on a Helix element goes through the
52
+ synthetic system to the native `change` the element sends, which is why the
53
+ ordinary path costs a consumer nothing to learn.
54
+
55
+ **The type map is global.** `HTMLElementEventMap` is shared with every element
56
+ in your application. Our classes extend `Event`, so an entry named `select`
57
+ would not fail to compile: it would silently retype
58
+ `textarea.addEventListener('select', …)` as carrying our detail, which at
59
+ runtime is `undefined`. Measured. An unprefixed name is not a collision we would
60
+ notice, it is one you would.
61
+
62
+ ## Propagation follows the event's role
63
+
64
+ **Lifecycle does not bubble and is not composed.** `helix-before-open` and
65
+ `helix-before-close`. Overlays nest, and an inner one's lifecycle must never reach the
66
+ outer one that contains it: a menu closing inside a dialog is not the dialog
67
+ closing.
68
+
69
+ **Coordination bubbles and is composed.** `helix-select`, `helix-dismiss`,
70
+ `helix-before-change`. A parent, or a router, can intercept one across a shadow
71
+ boundary, which is what makes a `Tree` usable as navigation.
72
+
73
+ If you are listening on a container and hearing nothing, the event is probably a
74
+ lifecycle one and the listener belongs on the element itself.
75
+
76
+ ## Canceling
77
+
78
+ The events that precede a state change are cancelable, and that is the only way
79
+ to refuse one:
80
+
81
+ ```ts
82
+ dialog.addEventListener('helix-before-close', (event) => {
83
+ if (unsaved) {
84
+ event.preventDefault()
85
+ }
86
+ })
87
+ ```
88
+
89
+ Every dispatched event is held to a declaration and every declaration to a
90
+ dispatch, so an event you can listen for is an event that is documented.
91
+
92
+ ## The set
93
+
94
+ | Event | Roughly |
95
+ |---|---|
96
+ | `helix-before-change` | A value about to move, refusable |
97
+ | `helix-before-toggle` | A checked state about to flip, refusable |
98
+ | `helix-before-open`, `helix-before-close` | An open state about to move, refusable |
99
+ | `helix-select` | An item chosen from a set |
100
+ | `helix-dismiss` | The reader asked for something to go away |
101
+ | `helix-next`, `helix-previous` | Movement through a sequence |
102
+ | `helix-filter` | A query changed in a filtering control |
103
+ | `helix-navigate` | A component is about to navigate |
104
+ | `helix-copy` | Something was copied |
105
+ | `helix-caret` | The cursor moved in a text control |
106
+ | `helix-load`, `helix-error`, `helix-complete` | Media and async lifecycle |
107
+ | `helix-create` | A new item was made from a query |
108
+ | `helix-console` | The sandbox relaying a log |
109
+ | `helix-tour-ask`, `helix-sample-start`, `helix-sample-end` | Tour and color sampling |
110
+
111
+ And the native ones every stateful component sends once the change has landed:
112
+ `input` and `change` on a value control, `toggle` on anything that opens,
113
+ carrying `newState` and `oldState` the way `<details>` does, `close` on a dialog,
114
+ and `load` and `error` on an image.
115
+
116
+ Each component's page lists the events it actually dispatches, with the class
117
+ and the detail.
118
+
119
+ ## In a framework
120
+
121
+ A native event is `onChange`, `onInput` or `onToggle`, which is what React and
122
+ every framework already understand. A prefixed one is `on` and the name,
123
+ verbatim: `onhelix-before-change`. Nothing strips the prefix and nothing
124
+ camel-cases it, which is React's own behavior rather than a convention here. See [React](./react), [Vue](./vue) and
125
+ [Svelte](./svelte).