@fusion.dev/helix-docs 0.205.2 → 0.207.0

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 (42) hide show
  1. package/README.md +22 -2
  2. package/content/angular.mdx +37 -4
  3. package/content/browser-support.mdx +3 -3
  4. package/content/building-on-the-headless-tier.mdx +84 -21
  5. package/content/charting.mdx +50 -0
  6. package/content/coding-agents.mdx +2 -1
  7. package/content/color-modes.mdx +2 -2
  8. package/content/color.mdx +35 -5
  9. package/content/diagnostics.mdx +255 -0
  10. package/content/graphs.mdx +6 -0
  11. package/content/iconography.mdx +31 -13
  12. package/content/installation.mdx +34 -10
  13. package/content/intent-reference.mdx +27 -3
  14. package/content/introduction.mdx +3 -2
  15. package/content/localization.mdx +86 -4
  16. package/content/maps.mdx +65 -13
  17. package/content/metrics.mdx +252 -249
  18. package/content/other-frameworks.mdx +52 -12
  19. package/content/overriding-a-component.mdx +11 -10
  20. package/content/react.mdx +15 -1
  21. package/content/recipes.mdx +18 -2
  22. package/content/sandbox.mdx +75 -25
  23. package/content/server-rendering.mdx +2 -2
  24. package/content/state-and-control.mdx +7 -1
  25. package/content/surfaces-and-separation.mdx +2 -1
  26. package/content/svelte.mdx +54 -8
  27. package/content/theme-builder.mdx +15 -3
  28. package/content/theming.mdx +5 -1
  29. package/content/tiers.mdx +15 -14
  30. package/content/using-helix-with-an-agent.mdx +12 -5
  31. package/content/using-the-elements.mdx +24 -23
  32. package/content/versioning.mdx +15 -8
  33. package/content/visual-axes.mdx +11 -4
  34. package/content/vue.mdx +28 -1
  35. package/content/without-a-bundler.mdx +9 -7
  36. package/dist/index.d.ts +1 -3
  37. package/dist/index.js +0 -1
  38. package/dist/pages.generated.js +7 -1
  39. package/dist/reference.d.ts +70 -6
  40. package/dist/reference.js +13 -2
  41. package/dist/sources.generated.js +1343 -458
  42. package/package.json +13 -12
@@ -8,9 +8,9 @@ trailing: true
8
8
 
9
9
  # Other frameworks
10
10
 
11
- This page covers Solid, Qwik, Lit, Ember, Alpine, htmx, and any other framework
12
- that puts elements in a document. They all follow the same three rules and need
13
- nothing else.
11
+ This page covers Preact, Solid, Qwik, Lit, Ember, Alpine, htmx, and any other
12
+ framework that puts elements in a document. They all follow the same three
13
+ rules and need nothing else.
14
14
 
15
15
  ## The three rules
16
16
 
@@ -44,11 +44,12 @@ in full.
44
44
 
45
45
  | Framework | Notes |
46
46
  |---|---|
47
- | Solid | Sets properties on custom elements by default. Use `prop:` to force one and `attr:` to force the other. `on:helix-before-change` binds the literal name |
48
- | Qwik | Works as JSX. Bind with `document:on…` or a ref, since a plain `on*` prop is normalized |
49
- | Lit | `.prop=`, `?bool=`, and `@helix-before-change=` work with no setup |
50
- | Ember | Angle-bracket invocation passes attributes only, so set properties from a modifier |
51
47
  | Alpine, htmx | Markup and attributes only, which work with every component. Refusing a change needs script |
48
+ | Ember | Angle-bracket invocation passes attributes only, so set properties from a modifier |
49
+ | Lit | `.prop=`, `?bool=`, and `@helix-before-change=` work with no setup |
50
+ | Preact | Sets a property when the element has one, and an attribute otherwise. `onhelix-before-change` binds the literal name. [Typings](#typings) says how to type the tags |
51
+ | Qwik | Works as JSX. Bind with `document:on…` or a ref, since a plain `on*` prop is normalized |
52
+ | Solid | Sets properties on custom elements by default. Use `prop:` to force one and `attr:` to force the other. `on:helix-before-change` binds the literal name. [Typings](#typings) says how to type the tags |
52
53
 
53
54
  We do not test these frameworks, so they share one page. If your framework needs
54
55
  something the three rules do not cover, tell us. It is a gap in this page or in
@@ -56,11 +57,50 @@ the library.
56
57
 
57
58
  ## Typings
58
59
 
59
- `@fusion.dev/helix/jsx` is the base for typing any framework that uses JSX, and
60
- the Svelte typings are built from it. If your framework needs a different
61
- format, use the manifest at `@fusion.dev/helix/custom-elements.json`. It
62
- describes every tag, prop, event, and part, and we generate all our typings from
63
- it.
60
+ `@fusion.dev/helix/jsx` types the tags for React, Preact, and Solid, and it is
61
+ the base for typing any other framework that uses JSX. The Svelte typings read
62
+ their props from it. It needs only the types of the framework you use, and it
63
+ compiles with `skipLibCheck: false`.
64
+
65
+ `ref` and `style` take the types of the framework the tag is written in:
66
+
67
+ - React: React's `Ref` and `CSSProperties`.
68
+ - Preact: Preact's `Ref`, and the `style` a `<div>` takes, which is a string or
69
+ an object.
70
+ - Solid: the element or a callback that receives it, and the `style` a `<div>`
71
+ takes, which is a string or an object.
72
+ - Any other framework: the element or a callback that receives it, and an
73
+ object of style properties.
74
+
75
+ In Preact and Solid, import the typings once, anywhere TypeScript sees it:
76
+
77
+ ```ts
78
+ import '@fusion.dev/helix/jsx'
79
+ ```
80
+
81
+ Preact reads the tags with `jsxImportSource: "preact"` and with `h` as the JSX
82
+ factory. If your `paths` alias `react` to `preact/compat`, the tags take `ref`
83
+ and `style` as `preact/compat` types React's, so `style` takes an object.
84
+
85
+ Solid reads the tags with `jsxImportSource: "solid-js"`, and each tag also
86
+ takes `prop:`, `attr:`, and `on:` forms of its props and events.
87
+
88
+ With `jsxImportSource: "solid-js/h"`, Solid runs without its compiler, and the
89
+ same import types the tags. Each prop also takes a function that returns its
90
+ value, and `h` calls that function again when a signal it reads changes. A
91
+ property whose value is a function, such as a slider's `formatter`, takes only a
92
+ function that returns it, because `h` calls any function it is given:
93
+
94
+ ```tsx
95
+ <helix-slider disabled={() => busy()} formatter={() => format} label="Volume" />
96
+ ```
97
+
98
+ `ref`, `children`, and the handlers named `on…`, such as
99
+ `on:helix-before-change`, take their values as they are.
100
+
101
+ If your framework needs a different format, use the manifest at
102
+ `@fusion.dev/helix/custom-elements.json`. It describes every tag, prop, event,
103
+ and part, and we generate all our typings from it.
64
104
 
65
105
  ## Register the elements
66
106
 
@@ -69,7 +69,7 @@ helix-select::part(trigger) {
69
69
  font-variant-numeric: tabular-nums;
70
70
  }
71
71
 
72
- helix-dialog::part(dialog) {
72
+ helix-dialog::part(surface) {
73
73
  max-inline-size: 40rem;
74
74
  }
75
75
  ```
@@ -99,16 +99,17 @@ helix-video::part(overflow-header) { … }
99
99
  ```
100
100
 
101
101
  `<helix-date-input>` renders a date picker in the `picker` role, so the picker's
102
- `cell` part is `picker-cell`. When the child's part is the role itself, the
103
- forwarded name is the role alone. The tooltip inside `<helix-copy-button>` is
104
- `tooltip`, and its arrow is `tooltip-arrow`.
102
+ `cell` part is `picker-cell`. The tooltip inside `<helix-copy-button>` is in the
103
+ `tooltip` role, so its `surface` is `tooltip-surface` and its arrow is
104
+ `tooltip-arrow`. When the child's part is the role itself, the forwarded name is
105
+ the role alone: each line inside `<helix-log>` is `line`.
105
106
 
106
107
  A part that changes with a state has a second name while the state is active,
107
108
  and that name is forwarded too. A tooltip placed below its button is also
108
- `tooltip-bottom`, and its arrow is also `tooltip-arrow-bottom`. Your stylesheet
109
- can use these names to style each side, which it cannot detect any other way.
110
- The arrow already faces the button on every side. Use the names for styles you
111
- add on top.
109
+ `tooltip-surface-bottom`, and its arrow is also `tooltip-arrow-bottom`. Your
110
+ stylesheet can use these names to style each side, which it cannot detect any
111
+ other way. The arrow already faces the button on every side. Use the names for
112
+ styles you add on top.
112
113
 
113
114
  **The prefix keeps part names unique.** A component and its child can use the
114
115
  same word. This happened once: `Command` forwarded the listbox's `search-field`
@@ -138,8 +139,8 @@ so you learn each name once:
138
139
  | `container` | An element's own outer box. |
139
140
  | `content` | A region you fill. |
140
141
  | `panel` | A region switched between, or a sheet anchored to an edge. |
141
- | `surface` | A floating panel drawn over the page. |
142
- | `viewport` | A scrollport the component reads the position of. |
142
+ | `surface` | A floating panel drawn over the page, such as a menu, a popover, a tooltip, or a dialog. |
143
+ | `viewport` | The box that shows part of a larger area: a scrollport, or the frame a map or a graph is panned and zoomed in. |
143
144
 
144
145
  A part that names something specific to one component, such as `thumb`,
145
146
  `swatch`, or `weekday`, is listed on that component's page.
package/content/react.mdx CHANGED
@@ -137,6 +137,8 @@ starts with that value and keeps applying its own changes, so the markup below
137
137
  works without a handler.
138
138
 
139
139
  ```tsx
140
+ import { flushSync } from 'react-dom'
141
+
140
142
  // Seeded: the element owns it from there.
141
143
  <helix-input default-value="Ada" label="Name" />
142
144
 
@@ -148,7 +150,7 @@ works without a handler.
148
150
  label="Name"
149
151
  onhelix-before-change={(event) => {
150
152
  event.preventDefault()
151
- setName(clean(event.detail.value))
153
+ flushSync(() => setName(clean(event.detail.value)))
152
154
  }}
153
155
  value={name}
154
156
  />
@@ -159,6 +161,18 @@ refuse it. React restores a native `<input>` whose state did not change, and it
159
161
  does not do this for a custom element. A handler that does not call `setState`
160
162
  leaves the element with the new value.
161
163
 
164
+ **Set the state inside `flushSync` when you write your own value.** React sets
165
+ the priority of a state update from the type of the event being handled.
166
+ `helix-before-change` is not one of the types React lists, so the update gets
167
+ the default priority and renders in a later task. A key that arrives before that
168
+ task lands in the text from before the update, and the keys typed in between are
169
+ lost. A barcode scanner, a password manager, and a test send keys that fast:
170
+ Playwright typing `Ada L!` with no delay into a field that keeps lowercase
171
+ letters left it empty. `flushSync` renders the update before your handler
172
+ returns, so the element holds your value when the next key arrives. The same
173
+ applies to every field you type into, such as `helix-textarea`,
174
+ `helix-number-input`, and `helix-pin-input`.
175
+
162
176
  *Until August 26, 2026, this page described the opposite behavior, and the typings
163
177
  enforced it. Writing `value` made you the owner of the state. The element
164
178
  applied no change until you set the value, and `<helix-input value="Ada" />`
@@ -140,7 +140,7 @@ Three Markdown constructs need components, because HTML alone cannot hold what
140
140
  they need:
141
141
 
142
142
  - **Fenced code** becomes `helix-code-block`, with the fence's language as its
143
- `label`. A `<pre>` has no label or copy button.
143
+ `label` and its `language`. A `<pre>` has no label or copy button.
144
144
  - **Alerts** such as `> [!WARNING]` become `helix-callout`. A `<blockquote>` has no
145
145
  tone. `NOTE` becomes `tone="info"`, `TIP` becomes `success`, `WARNING` becomes
146
146
  `warning`, and `CAUTION` becomes `danger`. `IMPORTANT` has no tone. Each
@@ -155,13 +155,29 @@ The `rehype` plugin from `@fusion.dev/helix/rehype` makes all three changes in t
155
155
  tree your Markdown library has already built. It parses nothing, so your
156
156
  library, its dialect, and your sanitizing stay yours. Add it to any pipeline
157
157
  that reaches rehype, such as `remark` with `remark-rehype`, `react-markdown`, or
158
- MDX:
158
+ MDX.
159
+
160
+ Tables are GitHub Flavored Markdown, so your pipeline needs `remark-gfm` to
161
+ parse them. Without it, a table stays a paragraph of pipes. For a `remark`
162
+ pipeline, install:
163
+
164
+ ```sh
165
+ bun add unified remark-parse remark-gfm remark-rehype rehype-stringify
166
+ ```
167
+
168
+ Then add the plugin after `remark-rehype`:
159
169
 
160
170
  ```js
161
171
  import rehypeHelix from '@fusion.dev/helix/rehype'
172
+ import rehypeStringify from 'rehype-stringify'
173
+ import remarkGfm from 'remark-gfm'
174
+ import remarkParse from 'remark-parse'
175
+ import remarkRehype from 'remark-rehype'
176
+ import { unified } from 'unified'
162
177
 
163
178
  const html = await unified()
164
179
  .use(remarkParse)
180
+ .use(remarkGfm)
165
181
  .use(remarkRehype)
166
182
  .use(rehypeHelix)
167
183
  .use(rehypeStringify)
@@ -12,8 +12,10 @@ You can edit every example on a component's page. When you change the markup,
12
12
  the preview redraws. When you change the stylesheet beside it, the preview
13
13
  restyles.
14
14
 
15
- To build a playground of your own, you would use the same elements. The
16
- isolation described below makes it safe to run markup that someone else wrote.
15
+ To build a playground of your own, use the same elements: the sandbox from
16
+ `@fusion.dev/helix/sandbox`, and the editor from `@fusion.dev/helix-sandbox`.
17
+ The isolation described below makes it safe to run markup that someone else
18
+ wrote.
17
19
 
18
20
  ## How the preview is isolated
19
21
 
@@ -160,7 +162,7 @@ preview, and `helix-caret` from it fills the status bar's position. A wrapper
160
162
  around CodeMirror:
161
163
 
162
164
  ```ts
163
- import { HelixBeforeEditEvent } from '@fusion.dev/helix'
165
+ import { HelixBeforeEditEvent } from '@fusion.dev/helix-core'
164
166
  import { EditorView, basicSetup } from 'codemirror'
165
167
 
166
168
  class CodeEditor extends HTMLElement {
@@ -217,16 +219,18 @@ own message from `:state(failed)`.
217
219
 
218
220
  ## The frame document
219
221
 
220
- `helix-sandbox-preview` loads `src` into a sandboxed frame. You provide that
221
- document. It loads the styles and the elements your examples use, and it talks
222
- to the preview through `postMessage`.
222
+ `helix-sandbox-preview` runs each example in a sandboxed frame. The frame
223
+ document either comes with the preview or is one you host.
223
224
 
224
- Without `src`, the preview loads a built-in document. It runs the example's
225
- HTML, CSS, and scripts, and reports the console, uncaught errors, the height,
226
- and presses, so a sandbox renders with nothing to host. It defines no element,
227
- so set `src` to a document of your own when your examples use Helix
228
- components. Its script is inline, so a page whose Content Security Policy
229
- refuses inline scripts needs a `src` too.
225
+ **Without `src`, the preview loads a built-in document**, so a sandbox needs
226
+ nothing hosted. It runs the example's HTML, CSS, and scripts, and reports the
227
+ console, uncaught errors, the height, and presses. It defines no element, so
228
+ set `src` to a document of your own when your examples use Helix components.
229
+ Its script is inline, so a page whose Content Security Policy refuses inline
230
+ scripts needs a `src` too.
231
+
232
+ **With `src`, the preview loads your document.** It loads the styles and the
233
+ elements your examples use, and it talks to the preview through `postMessage`.
230
234
 
231
235
  The messages:
232
236
 
@@ -250,9 +254,18 @@ it was given. The types are exported from `@fusion.dev/helix/sandbox` as
250
254
  you. Call it once in the frame document, after your elements are defined. It
251
255
  writes each render into `#preview-root`, runs the example's scripts, relays
252
256
  its console, its uncaught errors, and its unhandled promise rejections,
253
- reports its height and its presses, and adds the rules for the four layouts. A frame follows. `/helix.css` is the stylesheet from
254
- `@fusion.dev/helix-styles`, and `/frame.js` is a classic script you bundle,
255
- for the reason under "How the preview is isolated":
257
+ reports its height and its presses, and adds the rules for the four layouts.
258
+
259
+ The frame's own script must be a classic script, bundled into one file. A
260
+ module does not load there. The frame's origin is opaque, so a module import
261
+ back to your site is a cross-origin request, which fails unless your server
262
+ answers it with CORS headers. A stylesheet `<link>` is not checked, so the
263
+ styles load as they are.
264
+
265
+ A frame follows. `/frame.js` is the bundle. `/helix.css` is the stylesheet
266
+ from `@fusion.dev/helix-styles`, and it imports `primitives.css`, `tokens.css`,
267
+ `base.css`, and `elements.css` from beside it, so serve the five files from
268
+ the package's `dist` folder together:
256
269
 
257
270
  ```html
258
271
  <!doctype html>
@@ -284,16 +297,53 @@ Logs from the example appear in the console panel. Logs from the page around it
284
297
  do not. We patch the console inside the frame on purpose, so the panel shows
285
298
  what the example logs and nothing else.
286
299
 
287
- ## The package is private
300
+ ## The editor and the highlighter
301
+
302
+ `@fusion.dev/helix-sandbox` is the editor the component pages use, and the
303
+ highlighter that colors their code. Install it beside the library:
304
+
305
+ ```sh
306
+ npm install @fusion.dev/helix @fusion.dev/helix-styles @fusion.dev/helix-sandbox
307
+ ```
308
+
309
+ Each entry does one thing:
310
+
311
+ - `@fusion.dev/helix-sandbox/editor` registers `helix-sandbox-editor`, a
312
+ source pane with CodeMirror behind it.
313
+ - `@fusion.dev/helix-sandbox/highlight` turns a snippet into highlighted HTML,
314
+ on a server or in a page, with no editor.
315
+ - `@fusion.dev/helix-sandbox/code-block` colors every Code Block on the page.
316
+
317
+ The editor is a source pane, so it goes in the `source` slot, with a `name`
318
+ the sandbox reads:
319
+
320
+ ```html
321
+ <helix-sandbox>
322
+ <helix-sandbox-editor
323
+ language="html"
324
+ name="html"
325
+ slot="source"
326
+ value="<p>Hello</p>"
327
+ ></helix-sandbox-editor>
328
+ <helix-sandbox-preview slot="preview"></helix-sandbox-preview>
329
+ </helix-sandbox>
330
+ ```
331
+
332
+ ```ts
333
+ import '@fusion.dev/helix/sandbox'
334
+ import '@fusion.dev/helix-sandbox/editor'
335
+ ```
336
+
337
+ The [Sandbox reference](/helix/components/sandbox#helix-sandbox-editor) lists
338
+ the editor's properties, events, methods, and states.
288
339
 
289
- `@fusion.dev/helix-sandbox` is not published. It is the editor and spec runner
290
- for this repository, and it is Helix tooling. It depends on the repository:
340
+ `highlight` reads the same grammars as the editor. By default each token is a
341
+ span with Lezer's `tok-` class, for a stylesheet of your own. Pass
342
+ `syntaxHighlighter` for the editor's classes, and put `syntaxRules` in the
343
+ stylesheet where the result is drawn for the editor's colors:
291
344
 
292
- - The elements depend on the frame.
293
- - The frame depends on the build.
294
- - The build depends on this repository's layout.
345
+ ```ts
346
+ import { highlight, syntaxHighlighter } from '@fusion.dev/helix-sandbox/highlight'
295
347
 
296
- To add editable examples to your own documentation today, you would build the
297
- editor yourself, from the manifest and the `.examples` files. This is a gap we
298
- have not filled, and we have no policy against filling it. Tell us if you need
299
- it.
348
+ const markup = highlight('const total = 1', 'ts', syntaxHighlighter)
349
+ ```
@@ -85,8 +85,8 @@ Run your page's own scripts after the definitions load. Hydration expects that
85
85
  order. A property you set before hydration is one the server did not render
86
86
  with.
87
87
 
88
- `bun run check:spec:dsd` renders every example with Lit SSR, hydrates it in
89
- Chromium, and compares it with the same example rendered on a client.
88
+ We render every component example in both tiers with Lit SSR, hydrate it in
89
+ Chromium, and compare it with the same example rendered on a client.
90
90
 
91
91
  React server components do not use declarative shadow DOM. Their output is
92
92
  replayed through DOM APIs, and declarative shadow DOM does not activate for
@@ -49,12 +49,18 @@ state yourself, cancel the change and assign your own value:
49
49
  label="Name"
50
50
  onhelix-before-change={(event) => {
51
51
  event.preventDefault()
52
- setName(sanitize(event.detail.value))
52
+ flushSync(() => setName(sanitize(event.detail.value)))
53
53
  }}
54
54
  value={name}
55
55
  />
56
56
  ```
57
57
 
58
+ Assign your value before the next key arrives, or that key lands in the text
59
+ from before your value. Vue, Svelte, and Solid apply a bound value in time. In
60
+ React, set the state inside `flushSync`, as [React](./react) explains. In
61
+ Angular without zone.js, call `detectChanges()` after you set the signal, as
62
+ [Angular](./angular) explains.
63
+
58
64
  The platform uses the same pattern. You call `preventDefault()` to cancel:
59
65
 
60
66
  - a form submit
@@ -98,4 +98,5 @@ its own components, and the same applies to anything you build with it.
98
98
  The theme engine measures each ink against the ground it is actually painted
99
99
  on. The ladder records which pairs occur. For example, `foreground` on
100
100
  `surface-overlay` is a real pair, and `foreground-on-primary` on `surface` is
101
- not. See [Color](./color).
101
+ not. A field's fill, `well`, is translucent, so the text in a field is measured
102
+ against the well over each surface. See [Color](./color).
@@ -12,30 +12,74 @@ import '@fusion.dev/helix/svelte'
12
12
  ```
13
13
 
14
14
  Add this types-only import once. It augments `SvelteHTMLElements`, so every
15
- Helix tag is typed in a component. The import re-exports the JSX types. Svelte
16
- binds a handler as `onhelix-before-change={…}`, which is the shape those types
17
- already describe. With one shared copy of the types, the Svelte and JSX typings
18
- cannot disagree.
15
+ Helix tag is typed in a component. Svelte binds a handler as
16
+ `onhelix-before-change={…}`, which is the shape the JSX typings already
17
+ describe, so the Svelte typings read each tag's props from
18
+ `@fusion.dev/helix/jsx`. The two cannot disagree.
19
+
20
+ The typings need only Svelte's own types, and they compile with
21
+ `skipLibCheck: false`.
19
22
 
20
23
  ## Pass props and bind events
21
24
 
22
25
  ```svelte
23
- <helix-input label="Name" onchange={(event) => (name = event.target.value)} value={name} />
26
+ <helix-input
27
+ label="Name"
28
+ onchange={(event) => (name = event.currentTarget.value)}
29
+ value={name}
30
+ ></helix-input>
24
31
  ```
25
32
 
26
33
  Svelte sets a property when the element has one, and an attribute otherwise. It
27
34
  binds `on*` with `addEventListener`. Svelte shows no unknown-element warning,
28
35
  and you do not need a compiler flag.
29
36
 
37
+ Read the value from `event.currentTarget`, which is typed as the element.
38
+ `event.target` is an `EventTarget | null`, which has no `value`.
39
+
30
40
  Svelte passes objects and arrays as properties on its own:
31
41
 
32
42
  ```svelte
33
- <helix-data-table label="People" rows={people} />
43
+ <helix-data-table label="People" rows={people}></helix-data-table>
34
44
  ```
35
45
 
36
46
  Svelte's own attributes are typed on every Helix tag as well, so `onclick`,
37
47
  `class`, and a `style` string check as they do on a `div`.
38
48
 
49
+ ## Hold the element
50
+
51
+ `bind:this` holds the element, typed as its class:
52
+
53
+ ```svelte
54
+ <script lang="ts">
55
+ import type { HelixInput } from '@fusion.dev/helix'
56
+
57
+ let input: HelixInput | undefined = $state()
58
+ </script>
59
+
60
+ <helix-input bind:this={input} label="Name"></helix-input>
61
+ ```
62
+
63
+ ## Type a handler in the script
64
+
65
+ A handler written on the tag takes its event type from the tag. A handler
66
+ written in the script names its event type, which `@fusion.dev/helix/jsx`
67
+ exports for each element and event:
68
+
69
+ ```svelte
70
+ <script lang="ts">
71
+ import type { HelixInputHelixBeforeChangeElementEvent } from '@fusion.dev/helix/jsx'
72
+
73
+ const refuseLong = (event: HelixInputHelixBeforeChangeElementEvent) => {
74
+ if (event.detail.value.length > 40) {
75
+ event.preventDefault()
76
+ }
77
+ }
78
+ </script>
79
+
80
+ <helix-input label="Handle" onhelix-before-change={refuseLong}></helix-input>
81
+ ```
82
+
39
83
  ## Ignore the click warnings on a control
40
84
 
41
85
  `svelte-check` warns about an `onclick` on any Helix element. Its accessibility
@@ -55,8 +99,10 @@ right. Use a control there.
55
99
 
56
100
  These two rules apply in every framework.
57
101
 
58
- `bind:` does not work on a Helix control, because custom elements have no
59
- two-way binding convention to opt into. Read the value from the event.
102
+ Two-way bindings do not work on a Helix control, because custom elements have
103
+ no two-way binding convention to opt into. Svelte accepts `bind:value` only on
104
+ `<input>`, `<textarea>`, and `<select>`, and `bind:checked` only on `<input>`.
105
+ Read the value from the event.
60
106
 
61
107
  A declared `value`, `open`, `checked`, or `active` is a seed. The element starts
62
108
  with that value and then applies its own changes. To refuse a change, cancel the
@@ -73,9 +73,10 @@ its target. Its three tabs are:
73
73
  - **Contrast.** The pairs that fail, or a line saying that every pair passes.
74
74
  - **Collisions.** Colors too close to each other to tell apart, such as the
75
75
  primary color and a status color.
76
- - **Unverifiable.** Pairs whose background is translucent, so there is no known
77
- color to measure against. This is the cost of a translucent material, and
78
- the theme builder reports it.
76
+ - **Unverifiable.** Pairs whose background is translucent over something the
77
+ engine cannot know, such as a photograph or the page under a translucent
78
+ material, so there is no known color to measure against. This is the cost of
79
+ a translucent material, and the theme builder reports it.
79
80
 
80
81
  ## What the theme builder gives you
81
82
 
@@ -89,6 +90,17 @@ stylesheet:
89
90
  }
90
91
  ```
91
92
 
93
+ The block holds one mode, the one the preview shows, under the scope
94
+ `[data-theme="preview"]`. Put that attribute on the element to theme, or change
95
+ the selector. Under any selector other than `:root`, also add
96
+ `data-helix-theme` to the element. The block sets the seeds the stylesheet
97
+ derives the chart colors, the spacing, and the corner radii from, and the
98
+ stylesheet reads seeds only at `:root` or on an element with
99
+ `data-helix-theme`. See [Two ways to theme](./theming#two-ways-to-theme).
100
+
101
+ For a page with both modes, derive the intent with `deriveModes` or
102
+ `helix theme build`, as [Derive both modes](./theming#derive-both-modes) shows.
103
+
92
104
  The page's link holds every input you changed, so a link is the theme. To derive
93
105
  the theme in your own build, write the same inputs as an intent object:
94
106
 
@@ -29,7 +29,11 @@ changing them one at a time. A change to one value can break the contrast of a
29
29
  component elsewhere, and nothing tells you.
30
30
 
31
31
  `@fusion.dev/helix-theme` computes a complete token set from a few intent
32
- inputs, and every text and background pair meets its contrast target.
32
+ inputs. It solves each text color against the backgrounds it lands on, measures
33
+ every pair in its surface model, and reports any pair that misses its contrast
34
+ target. `foreground-subtle` is not in the model. It is a fixed step of the
35
+ stylesheet's gray ramp, and the engine does not solve it, so its contrast is not
36
+ guaranteed.
33
37
 
34
38
  ## Two ways to theme
35
39
 
package/content/tiers.mdx CHANGED
@@ -21,8 +21,8 @@ depends only on the packages below it.
21
21
  Neither is a tier, and you do not need either one to render a component.
22
22
 
23
23
  There is no React package. We generate the framework typings into
24
- `@fusion.dev/helix`, and the bridge that repairs React's hydration of custom
25
- events is in `@fusion.dev/helix-core`. See [React](./react).
24
+ `@fusion.dev/helix` and `@fusion.dev/helix-headless`, and the bridge that
25
+ repairs React's hydration of custom events is in `@fusion.dev/helix-core`. See [React](./react).
26
26
 
27
27
  ## Why the tiers are separate packages
28
28
 
@@ -64,7 +64,7 @@ export class HelixHeadlessAccordionItem extends MixinControllerOpen(HelixElement
64
64
  }
65
65
  }
66
66
 
67
- // web/src/accordion/root/accordion-root.ts
67
+ // styled/src/accordion/root/accordion-root.ts
68
68
  // extends the headless one, adds presentation only
69
69
  import { styles } from './accordion-root.css.js'
70
70
 
@@ -78,7 +78,7 @@ export class HelixAccordion extends HelixHeadlessAccordion {
78
78
  ```
79
79
 
80
80
  ```css
81
- /* web/src/accordion/item/accordion-item.css */
81
+ /* styled/src/accordion/item/accordion-item.css */
82
82
  [part='trigger'] {
83
83
  min-block-size: var(--helix-size-control);
84
84
  padding: var(--helix-space-sm);
@@ -115,8 +115,8 @@ as unstyled text and then changes into a card. The delay is short. It is most
115
115
  visible on a server-rendered page, where the markup arrives long before the
116
116
  JavaScript.
117
117
 
118
- `@fusion.dev/helix` includes `elements.css`, which hides each of its tags until
119
- the tag's definition loads:
118
+ `@fusion.dev/helix-styles` includes `elements.css`, which hides each tag of the
119
+ styled tier until the tag's definition loads:
120
120
 
121
121
  ```css
122
122
  helix-card:not(:defined) {
@@ -124,7 +124,7 @@ helix-card:not(:defined) {
124
124
  }
125
125
  ```
126
126
 
127
- **The base tier ships no such file on purpose.** There are three reasons, and
127
+ **The headless tier ships no such file on purpose.** There are three reasons, and
128
128
  the third one affects you most.
129
129
 
130
130
  First, hiding an element is a design choice. Below the styled tier, we ship only
@@ -138,20 +138,21 @@ inside, because there is no shadow root before the upgrade. The rule has to
138
138
  live in your stylesheet, so the choice is yours.
139
139
 
140
140
  Third, one file for every tag would not fit your project. You import only the
141
- base elements you use, so a generated list would include tags you never ship.
141
+ headless elements you use, so a generated list would include tags you never ship.
142
142
  Write the rule for the elements you have:
143
143
 
144
144
  ```css
145
- helix-base-accordion:not(:defined),
146
- helix-base-dialog:not(:defined) {
145
+ helix-headless-accordion:not(:defined),
146
+ helix-headless-dialog:not(:defined) {
147
147
  visibility: hidden;
148
148
  }
149
149
  ```
150
150
 
151
- Use `visibility` so the element keeps its place in the layout and the page does
152
- not reflow when it upgrades. `display: none` collapses the box, and the content
153
- jumps in when the definition arrives. That replaces a flash of unstyled text
154
- with a shift of everything below it.
151
+ Use `visibility`, so a hidden element keeps the size of its unstyled content.
152
+ The layout still settles when the element upgrades and takes its own size.
153
+ `display: none` collapses the box, so the element takes no space until the
154
+ definition arrives, and then everything below it shifts by the element's full
155
+ size.
155
156
 
156
157
  You may want to hide nothing. If your first paint matters more than the final
157
158
  layout, or your page has to be readable with JavaScript turned off, let the
@@ -43,8 +43,11 @@ writes where an installed family stands in for it, as `native-element`.
43
43
 
44
44
  Each family's API is in the packages you installed, and on this site:
45
45
 
46
- - `helix api family <family>`, or the MCP tool `helix_family`
47
- - `node_modules/@fusion.dev/helix/api.json`
46
+ - `helix api family <family>`, or the MCP tool `helix_family`, which read every
47
+ Helix package you installed
48
+ - `node_modules/@fusion.dev/helix/api.json`, and the `api.json` of each other
49
+ Helix package you installed, which lists the elements it adds to a family,
50
+ such as `helix-sandbox-editor` in `@fusion.dev/helix-sandbox`
48
51
  - `https://fusion.dev/helix/components/<family>.md` and `.json`
49
52
 
50
53
  Each lists the import lines, every tag, every property with its literal union,
@@ -98,12 +101,15 @@ block a change, call `preventDefault()` on the matching `helix-before-*` event:
98
101
  label="Name"
99
102
  onhelix-before-change={(event) => {
100
103
  event.preventDefault()
101
- setName(clean(event.detail.value))
104
+ flushSync(() => setName(clean(event.detail.value)))
102
105
  }}
103
106
  value={name}
104
107
  />
105
108
  ```
106
109
 
110
+ Set the state inside `flushSync` from `react-dom`. Without it, React renders the
111
+ update in a later task, and keys typed faster than that are lost.
112
+
107
113
  A form reset returns the element to `default-value` and the matching attributes
108
114
  for the other states. See [State and control](./state-and-control).
109
115
 
@@ -213,12 +219,13 @@ Run these after you write Helix code, and fix every finding at `error` and
213
219
 
214
220
  | Command | MCP tool | What it finds |
215
221
  |---|---|---|
216
- | `helix scan` | `helix_check` | Unknown tags, attributes, values, slots, parts, hooks, events, and tokens, a native element a Helix family replaces, a missing import, and the rest, each with its file and line |
222
+ | `helix scan` | `helix_check` | Unknown tags, attributes, values, slots, parts, hooks, events, and tokens, a native element a Helix family replaces, a missing import, a button inside a button, `disabled="false"`, and the rest, each with its file and line |
217
223
  | `helix diagnostics --once <url>` | `helix_check_page` | The same on the running page, and what the library logged as it ran |
218
224
  | `helix theme audit` | `helix_audit_theme` | Every foreground and background pair in your theme that misses its contrast target |
219
225
 
220
226
  `helix scan --file <path>` reports one file's findings and still reads the whole
221
- project, so an import in another file counts.
227
+ project, so an import in another file counts. [Diagnostics](./diagnostics)
228
+ lists every kind of finding, and what the checks do not cover.
222
229
 
223
230
  ## What this guide does not cover
224
231