@fusion.dev/helix-docs 0.223.0 → 0.224.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.
@@ -24,7 +24,7 @@ element.items = tabs
24
24
  ```
25
25
 
26
26
  **Bind events with `addEventListener`, using the event's own name.** Helix event
27
- names, such as `helix-before-open`, `helix-before-change`, and `helix-select`,
27
+ names, such as `helix-before-open`, `helix-before-change`, and `helix-before-select`,
28
28
  are lowercase and hyphenated. They have no camel-case variant. Your framework's
29
29
  `on*` syntax needs to pass that exact name through.
30
30
 
@@ -6,15 +6,15 @@ title: Routing and navigation
6
6
 
7
7
  # Routing and navigation
8
8
 
9
- Some components navigate, such as a tree used as a sidebar, a button that opens
10
- another page, or a tab that is a page. Each of these should be a link. A
9
+ Some components navigate, such as a tree used as a sidebar, a menu item that
10
+ opens another page, or a tab that is a page. Each of these should be a link. A
11
11
  framework router does not handle a link inside a shadow root the way it handles
12
12
  its own links.
13
13
 
14
14
  ## A component that navigates renders its own anchor
15
15
 
16
- `Tree.Item` and `Button` both take an `href`. With an `href`, the element you
17
- press *is* an `<a>`:
16
+ `Tree.Item`, `Tab`, and `Menu.Item` take an `href`. With an `href`, the element
17
+ you press *is* an `<a>`:
18
18
 
19
19
  ```html
20
20
  <helix-tree-item
@@ -103,9 +103,11 @@ changes the structure of the markup as well as adding an attribute. Without an
103
103
  `href`, nothing moves. The wrapper stays the `treeitem` and contains its group,
104
104
  the same way an `<li>` contains a `<ul>`.
105
105
 
106
- Tabs have no equivalent published pattern. Web Awesome has the same problem. Its
107
- answer is to drop the anchor and navigate in JavaScript. That works, and you
108
- lose every item in the list above.
106
+ A tab with an `href` renders `role="tab"` on the `<a>` itself, so a press,
107
+ Enter, and the browser's own link gestures follow it. It sends `helix-before-select`
108
+ first, and canceling that keeps the browser from following, so a client-side
109
+ router can follow it instead. Web Awesome drops the anchor and navigates in
110
+ JavaScript, which loses every item in the list above.
109
111
 
110
112
  ## Use client-side routing with real anchors
111
113
 
@@ -166,9 +168,14 @@ That is expected.
166
168
 
167
169
  | Component | Notes |
168
170
  |---|---|
169
- | `Button` | Renders an `<a>` styled as a button. |
170
171
  | `Tree.Item` | Changes the ARIA shape, as above. A linked branch navigates from its row and opens from its chevron. |
171
172
  | `Menu.Item` | The row becomes the anchor and carries `menuitem`. Takes `target`, `rel` and `download`. |
173
+ | `Tab` | The tab becomes the anchor and carries `tab`. Takes `target` and `rel`. `helix-before-select` comes first, and canceling it keeps the browser from following. |
174
+
175
+ `Button` never renders an anchor, because a router renders its own `<a>` and
176
+ cannot render one inside the button's shadow root. For a link that looks like a
177
+ button, put the `data-button` recipe on your own anchor, such as
178
+ `<Link data-button="solid" href="/pricing">`, which draws the same control.
172
179
 
173
180
  Other components that should navigate do not take an `href` yet. To make one
174
181
  navigate, slot a link into it. `Tree.Item` follows a slotted anchor when you
@@ -204,8 +211,8 @@ it cannot work here.
204
211
 
205
212
  No arrangement of the markup meets both of these constraints.
206
213
 
207
- **In a command palette, navigate from the `helix-select` event.** Listen for
208
- `helix-select` on `helix-command` and call your router. A palette entry is a
214
+ **In a command palette, navigate from the `helix-before-select` event.** Listen for
215
+ `helix-before-select` on `helix-command` and call your router. A palette entry is a
209
216
  command, and "go to Settings" is a command that navigates. Command palettes in
210
217
  other applications work the same way. You lose the gestures that only a real
211
218
  anchor has:
@@ -182,7 +182,7 @@ preview, and `helix-caret` from it fills the status bar's position. A wrapper
182
182
  around CodeMirror:
183
183
 
184
184
  ```ts
185
- import { HelixBeforeEditEvent } from '@fusion.dev/helix-core'
185
+ import { HelixBeforeEditEvent } from '@fusion.dev/helix/sandbox/events'
186
186
  import { EditorView, basicSetup } from 'codemirror'
187
187
 
188
188
  class CodeEditor extends HTMLElement {
@@ -122,7 +122,7 @@ casing must match. Outside React, use
122
122
 
123
123
  **Lifecycle events do not bubble. Coordination events do.**
124
124
  `helix-before-open` and `helix-before-close` reach the element and stop there,
125
- so a menu closing inside a dialog does not close the dialog. `helix-select`,
125
+ so a menu closing inside a dialog does not close the dialog. `helix-before-select`,
126
126
  `helix-before-change`, and `helix-dismiss` cross shadow boundaries, so you can
127
127
  listen for them on a container. If a listener on a wrapper receives nothing, the
128
128
  event is a lifecycle event. Put the listener on the element.
@@ -36,12 +36,37 @@ The bug is [facebook/react#35446](https://github.com/facebook/react/issues/35446
36
36
  and it is still open. The elements work around it for you.
37
37
 
38
38
  Every Helix element binds the handler itself, just before it dispatches any
39
- event, using the function React already holds for it. We measured this in
40
- Chrome against React 19.2.8. On a hydrated page, the first event reaches the
41
- handler. After React binds its own listener, no event is delivered twice.
39
+ event, using the function React already holds for it. On a hydrated page, the
40
+ first event reaches the handler. After React binds its own listener, no event
41
+ is delivered twice.
42
42
 
43
43
  You do not need to do anything, and nothing changes when React fixes the bug.
44
44
 
45
+ We measured this in Chromium against React 19.3.1 on October 25, 2026. Each
46
+ version below rendered a page with `renderToString` and hydrated it with
47
+ `hydrateRoot`:
48
+
49
+ | React | Bug present | Helix handles it |
50
+ | --- | --- | --- |
51
+ | 19.0.9 | Yes | Yes |
52
+ | 19.2.9 | Yes | Yes |
53
+ | 19.3.1 | Yes | Yes |
54
+ | 19.3.0 canary of October 23, 2026 | Yes | Yes |
55
+ | 18.3.1 | Yes | The first event only |
56
+
57
+ Helix supports React 19. React 18 binds no `on` prop on a custom element at
58
+ all. Helix binds the first handler, but each render that passes a new function
59
+ adds another listener, so a handler runs once for every render it has been
60
+ through.
61
+
62
+ In a development build, the page logs an error when React has a root and no
63
+ Helix element finds the props React holds for it. This means the installed
64
+ React stores its props under a key Helix does not read, and every `onhelix-*`
65
+ handler on a server-rendered page is dead. Pin React to a version in the table,
66
+ and open an issue at
67
+ [github.com/project-fusion/fusion](https://github.com/project-fusion/fusion/issues)
68
+ that names the version you installed.
69
+
45
70
  React's own events work when they bubble. `onClick` on a `<helix-button>` goes
46
71
  through React's synthetic event system, which hydration sets up.
47
72
 
@@ -63,8 +88,8 @@ written.
63
88
  - `onhelix-sample-start` reaches `helix-sample-start`. ✅
64
89
  - `onHelixOpen` binds an event called `HelixOpen`, which is never dispatched. ❌
65
90
 
66
- The number of hyphens does not matter. We measured this in Chrome against React
67
- 19.2.8.
91
+ The number of hyphens does not matter. We measured this in Chromium against
92
+ React 19.3.1.
68
93
 
69
94
  ## Register the elements in one client component
70
95
 
@@ -23,7 +23,13 @@ npx playwright install --with-deps chromium
23
23
 
24
24
  Capture runs in Playwright, which you install beside the command line. The
25
25
  rest of `helix` needs no browser, so Playwright is an optional peer, and
26
- `helix visual test` names it when it is missing.
26
+ `helix visual test` names it when it is missing. A config in TypeScript needs
27
+ Node 22.18 or later. On an older Node, write it as `visual.config.mjs`.
28
+
29
+ **On macOS and Windows, a run skips until you choose where baselines are taken.**
30
+ `test` and `update` say so, compare nothing, and exit 0, because text draws
31
+ differently on each platform. Take baselines on Linux, or set `platform: 'any'`
32
+ to compare on the machine in front of you.
27
33
 
28
34
  ## Commands
29
35
 
@@ -54,6 +60,38 @@ Each run ends with one line that says what it compared and what it skipped.
54
60
  `helix --json visual …` prints one JSON document on stdout and moves the
55
61
  sentences to stderr.
56
62
 
63
+ ## Picture a site
64
+
65
+ Picture a production build, because a dev server draws its own overlays, and they
66
+ differ from run to run. `visual.config.ts`, for a Next.js site:
67
+
68
+ ```ts
69
+ import { defineConfig } from '@fusion.dev/helix-cli/visual'
70
+
71
+ export default defineConfig({
72
+ sources: [
73
+ {
74
+ baselines: 'visual/baselines',
75
+ kind: 'server',
76
+ name: 'site',
77
+ routes: ['/', '/pricing', { name: 'not-found', path: '/nope', status: 404 }],
78
+ start: 'npm run start -- --port 3100',
79
+ url: 'http://localhost:3100',
80
+ },
81
+ ],
82
+ })
83
+ ```
84
+
85
+ ```sh
86
+ npm run build
87
+ npx helix visual update
88
+ ```
89
+
90
+ `update` writes the baselines and the fingerprint, and its report shows each
91
+ picture it wrote. Look at them, commit them, and run `helix visual test` from then
92
+ on. Any page that answers 400 or above fails the entry, unless its route names that
93
+ `status`.
94
+
57
95
  ## The config
58
96
 
59
97
  `visual.config.ts`, in the directory you run `helix visual` from:
@@ -90,9 +128,28 @@ export default defineConfig({
90
128
  width, and dark when you give none. Each baseline is written beside its
91
129
  entry as `<stem>.<name>.png`.
92
130
  - **`serve`** is URL paths served from disk, relative to the config.
131
+ - **`baselines`** is, on a server source, the directory its baselines are written
132
+ in, relative to the config.
133
+ - **`platform`** is where baselines are taken and compared: `linux` unless you set
134
+ `any`.
135
+ - **`network`** is what a page may reach. `origin`, the default, refuses every
136
+ request to another host and names each host in the log and in `run.json`.
137
+ `{ allow: ['tiles.example.com'] }` lets those hosts through, and `open` lets every
138
+ host through.
139
+ - **`mask`** is selectors whose elements are painted over in every picture, for
140
+ what changes on its own, such as a clock. A mask paints over the element's box and
141
+ anything drawn on top of it. The run names a mask that matches nothing on a page,
142
+ or only elements with no box.
143
+ - **`style`** is a stylesheet applied while each picture is taken, to hold still
144
+ what `mask` cannot.
145
+ - **`settle.selector`** names what a page matches once it has drawn what it draws
146
+ late, such as a map's tiles. The entry fails when nothing matches within
147
+ `settle.timeout`, 30 seconds unless you set it.
93
148
  - **`settle.readyProperty`** names a property your elements carry holding a
94
149
  promise that resolves once they have rendered. The run awaits it before each
95
- shot.
150
+ shot, for 5 seconds, and then names the tags still waiting.
151
+ - **`settle.elements`** limits the custom elements a run waits for to one tag
152
+ prefix, for a framework that writes hyphenated tags it never defines.
96
153
  - **`aria`** keeps each entry's accessibility snapshot beside its baselines
97
154
  and compares it as text, so a lost label fails even when no pixel moved.
98
155
  - **`checks`** adds judgments that need no baseline: `code` fails a code block
@@ -112,7 +112,7 @@ export const docPages = [
112
112
  title: 'Intent reference',
113
113
  },
114
114
  {
115
- description: 'What Helix is, and the packages it includes.',
115
+ description: 'What Fusion Helix is, and the packages it includes.',
116
116
  order: 1,
117
117
  section: 'Getting started',
118
118
  slug: 'introduction',