@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.
- package/LICENSE +21 -0
- package/content/accessibility.mdx +117 -0
- package/content/angular.mdx +60 -0
- package/content/browser-support.mdx +83 -0
- package/content/building-on-the-headless-tier.mdx +143 -0
- package/content/charting.mdx +93 -0
- package/content/coding-agents.mdx +139 -0
- package/content/color-modes.mdx +87 -0
- package/content/color.mdx +92 -0
- package/content/composition.mdx +171 -0
- package/content/elevation-and-material.mdx +93 -0
- package/content/events.mdx +125 -0
- package/content/forms.mdx +170 -0
- package/content/iconography.mdx +80 -0
- package/content/installation.mdx +125 -0
- package/content/intent-reference.mdx +107 -0
- package/content/introduction.mdx +69 -0
- package/content/localization.mdx +95 -0
- package/content/metrics.mdx +399 -0
- package/content/motion.mdx +91 -0
- package/content/other-frameworks.mdx +70 -0
- package/content/overlays.mdx +102 -0
- package/content/overriding-a-component.mdx +160 -0
- package/content/react.mdx +144 -0
- package/content/recipes.mdx +131 -0
- package/content/routing.mdx +208 -0
- package/content/sandbox.mdx +76 -0
- package/content/server-rendering.mdx +103 -0
- package/content/shape-and-borders.mdx +80 -0
- package/content/size-and-density.mdx +86 -0
- package/content/space-and-size.mdx +89 -0
- package/content/state-and-control.mdx +132 -0
- package/content/surfaces-and-separation.mdx +99 -0
- package/content/svelte.mdx +45 -0
- package/content/theme-builder.mdx +66 -0
- package/content/theming.mdx +111 -0
- package/content/tiers.mdx +159 -0
- package/content/tokens.mdx +88 -0
- package/content/typography.mdx +117 -0
- package/content/using-helix-with-an-agent.mdx +231 -0
- package/content/using-the-elements.mdx +125 -0
- package/content/versioning.mdx +72 -0
- package/content/visual-axes.mdx +75 -0
- package/content/vue.mdx +80 -0
- package/content/without-a-bundler.mdx +119 -0
- package/dist/catalog.d.ts +186 -0
- package/dist/catalog.js +296 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +6 -0
- package/dist/naming.d.ts +14 -0
- package/dist/naming.js +28 -0
- package/dist/pages.generated.d.ts +7 -0
- package/dist/pages.generated.js +284 -0
- package/dist/reference.d.ts +105 -0
- package/dist/reference.js +181 -0
- package/dist/registry.d.ts +89 -0
- package/dist/registry.js +70 -0
- package/dist/sections.d.ts +44 -0
- package/dist/sections.js +36 -0
- package/dist/sources.generated.d.ts +6 -0
- package/dist/sources.generated.js +5158 -0
- package/package.json +46 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: The four things that look like a library bug and are not, and where an agent should read from.
|
|
3
|
+
parent: coding-agents
|
|
4
|
+
section: Getting started
|
|
5
|
+
title: Using Helix with an agent
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Using Helix with an agent
|
|
10
|
+
|
|
11
|
+
**A page rather than a skill file, and the format is the point.** This started as
|
|
12
|
+
`.claude/skills/using-helix/SKILL.md`, which is one vendor's format sitting in a
|
|
13
|
+
repository, and every other vendor has its own: Cursor reads `.cursor/rules`,
|
|
14
|
+
Copilot reads `.github/copilot-instructions.md`, and a consumer of the published
|
|
15
|
+
packages has none of them because they have this site and no checkout.
|
|
16
|
+
|
|
17
|
+
So the content lives once, here, at a URL that [llms.txt](https://fusion.dev/llms.txt) indexes and
|
|
18
|
+
that every one of those formats can point at. Paste the link into whichever file
|
|
19
|
+
your tool reads. What is vendor-neutral and executable is [the MCP
|
|
20
|
+
server](./coding-agents), which Claude, Cursor, Windsurf and Zed all speak.
|
|
21
|
+
|
|
22
|
+
## Read the manifest before writing a tag
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
https://fusion.dev/helix/custom-elements.json
|
|
26
|
+
@fusion.dev/helix/custom-elements.json
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Every tag, every property with its literal union, every event with its class,
|
|
30
|
+
every slot, part and custom property. **The vocabulary is the file**: a tag that
|
|
31
|
+
is not in it does not exist, and a property value outside a declared union is one
|
|
32
|
+
the component rejects. Unions are literal rather than named for exactly this
|
|
33
|
+
reason, so `'ghost' | 'outlined' | 'solid' | 'subtle'` answers the question where
|
|
34
|
+
`ButtonEmphasis` sends you somewhere else.
|
|
35
|
+
|
|
36
|
+
One family as data, when the whole manifest is more than the question needs:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
https://fusion.dev/helix/components/<family>.json
|
|
40
|
+
https://fusion.dev/helix/components/<family>.md
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The `.md` carries the API tables **and the examples**, which the HTML page
|
|
44
|
+
deliberately does not. Start from `https://fusion.dev/llms.txt`, which is one
|
|
45
|
+
request and names every document plus these two surfaces.
|
|
46
|
+
|
|
47
|
+
## The four things that go wrong
|
|
48
|
+
|
|
49
|
+
These are in order of how often they look like a library bug and are not.
|
|
50
|
+
|
|
51
|
+
### 1. A declared `value` hands you the control
|
|
52
|
+
|
|
53
|
+
```text
|
|
54
|
+
<!-- Ignores every keystroke. Nothing is logged. Nothing looks wrong. -->
|
|
55
|
+
<helix-input label="Name" value="Ada"></helix-input>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Do not paste it. Plain text rather than a live example, the same way
|
|
59
|
+
[State and control](./state-and-control) shows this one: a reader copies what is
|
|
60
|
+
in front of them, and these are the two pages that have to show the mistake in
|
|
61
|
+
order to name it.
|
|
62
|
+
|
|
63
|
+
**Writing `value`, `open`, `checked` or `active` is a claim of ownership.** From
|
|
64
|
+
then on the element announces changes and applies none of them, waiting to be
|
|
65
|
+
told. Which mode it is in is read once, at its first update.
|
|
66
|
+
|
|
67
|
+
Seed with the `default-` twin, or answer the event:
|
|
68
|
+
|
|
69
|
+
```html
|
|
70
|
+
<helix-input
|
|
71
|
+
default-value="Ada"
|
|
72
|
+
label="Name"
|
|
73
|
+
></helix-input>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
<helix-input label="Name" onChange={(e) => setName(e.currentTarget.value)} value={name} />
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
In TypeScript this is a compile error rather than a surprise. Everywhere else the
|
|
81
|
+
element warns on its second unanswered request.
|
|
82
|
+
|
|
83
|
+
### 2. The event name is the event's own
|
|
84
|
+
|
|
85
|
+
`onhelix-before-open`, not `onHelixOpen` and not `onOpen`. React slices `on` off the
|
|
86
|
+
prop and listens for exactly what is left, so the casing is not negotiable. From
|
|
87
|
+
anything that is not React, `addEventListener('helix-before-open', …)`.
|
|
88
|
+
|
|
89
|
+
**Lifecycle does not bubble and coordination does.** `helix-before-open` and
|
|
90
|
+
`helix-before-close` reach the element and stop, so a menu closing inside a dialog is
|
|
91
|
+
not the dialog closing; `helix-select`, `helix-before-change` and `helix-dismiss` cross
|
|
92
|
+
shadow boundaries and can be caught on a container. If a listener on a wrapper
|
|
93
|
+
hears nothing, it is a lifecycle event and belongs on the element.
|
|
94
|
+
|
|
95
|
+
### 3. `:disabled`, never `[disabled]`
|
|
96
|
+
|
|
97
|
+
```css
|
|
98
|
+
helix-input:disabled { … } /* right: matches a disabled fieldset too */
|
|
99
|
+
helix-input[disabled] { … } /* wrong: a fieldset sets no attribute */
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
A control inside `<fieldset disabled>` is disabled by the platform and carries no
|
|
103
|
+
attribute, so the attribute selector paints it as live while it refuses input.
|
|
104
|
+
|
|
105
|
+
### 4. Two stylesheets, then the elements
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import '@fusion.dev/helix/helix.css'
|
|
109
|
+
import '@fusion.dev/helix'
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
`helix.css` is `primitives.css`, `tokens.css`, `base.css` and `elements.css` in
|
|
113
|
+
the order they have to be in. Write them out yourself only to substitute one.
|
|
114
|
+
|
|
115
|
+
`elements.css` is the one people leave out, and everything works without it
|
|
116
|
+
until the page is server-rendered: an element that has not upgraded has no
|
|
117
|
+
styles of its own, so the markup flashes unstyled and then snaps into place.
|
|
118
|
+
|
|
119
|
+
The tell for a missing layer is which. A flash before hydration is
|
|
120
|
+
`elements.css`; bare headings are `base.css`. **No color anywhere means the
|
|
121
|
+
ramp or the semantic layer, and the two are told apart by what is in the
|
|
122
|
+
stylesheet rather than by what is on screen**: without `tokens.css` nothing
|
|
123
|
+
declares `--helix-color-*` at all, while without `primitives.css` all 177 are
|
|
124
|
+
declared and every one resolves to `rgba(0, 0, 0, 0)`, since `tokens.css` refers
|
|
125
|
+
to a primitive 235 times and to nothing else. The second is the one that looks
|
|
126
|
+
like a working install, and it is the reason `all.css` exists.
|
|
127
|
+
|
|
128
|
+
## Choosing a family
|
|
129
|
+
|
|
130
|
+
Ask the manifest, not your memory. Each family's `helix.aliases` lists the other
|
|
131
|
+
names it goes by, which is usually how a request arrives: `Segment` and `NavItem`
|
|
132
|
+
are `Toggle`, `Key` is `Legend`, `Hint` is `Tooltip`.
|
|
133
|
+
|
|
134
|
+
Two distinctions worth knowing before searching:
|
|
135
|
+
|
|
136
|
+
- **A group is a part when it reports a form value** and its own family when it
|
|
137
|
+
does not. `Radio.Group` and `Toggle.Group` are parts; `Checkbox Group` and
|
|
138
|
+
`Button Group` are families whose members submit themselves.
|
|
139
|
+
- **A control has no `size`.** Height comes from `data-density` on the region,
|
|
140
|
+
`sm` / `md` / `lg`, which crosses shadow boundaries through custom properties.
|
|
141
|
+
If two controls in a row must differ in height, the layout is wrong rather than
|
|
142
|
+
the API.
|
|
143
|
+
|
|
144
|
+
## Styling from the outside
|
|
145
|
+
|
|
146
|
+
Three doors, in the order to try them.
|
|
147
|
+
|
|
148
|
+
1. **A token.** `--helix-radii-md: 2px` at `:root` reaches every component,
|
|
149
|
+
because custom properties inherit. Most "can I restyle the button" is really
|
|
150
|
+
"our brand has less rounding".
|
|
151
|
+
2. **A per-component hook.** `--card-padding` on `helix-card`. Named after the
|
|
152
|
+
component, documented on its page, defaults to a semantic token.
|
|
153
|
+
3. **A part.** `helix-select::part(trigger)`. The most powerful and the one that
|
|
154
|
+
couples you to internal structure.
|
|
155
|
+
|
|
156
|
+
**Never read a primitive.** `--_helix-neutral-500` and its kind belong to
|
|
157
|
+
`@fusion.dev/helix-styles`; the underscore says so and the indirection is the point.
|
|
158
|
+
|
|
159
|
+
## No custom elements at all
|
|
160
|
+
|
|
161
|
+
`@fusion.dev/helix-styles` is plain CSS with no JavaScript, and it carries recipes for
|
|
162
|
+
markup Helix does not render:
|
|
163
|
+
|
|
164
|
+
```html
|
|
165
|
+
<button
|
|
166
|
+
data-button="solid"
|
|
167
|
+
data-button-tone="primary"
|
|
168
|
+
>Save</button>
|
|
169
|
+
<article data-prose><h2>From a CMS</h2></article>
|
|
170
|
+
<div data-density="sm">…</div>
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
A recipe has appearance and no behavior: no state, no form participation, no
|
|
174
|
+
focus trapping, no keyboard handling. If you are adding script to one, the
|
|
175
|
+
component is what you wanted.
|
|
176
|
+
|
|
177
|
+
## Pointing your tool at this page
|
|
178
|
+
|
|
179
|
+
**A line, not a copy.** Every tool that reads project instructions reads a file
|
|
180
|
+
in your repository, and they all read a different one. What goes in it is a
|
|
181
|
+
pointer to this URL rather than its contents: a copy starts going stale the
|
|
182
|
+
moment it is pasted, and this page changes with the library.
|
|
183
|
+
|
|
184
|
+
```md
|
|
185
|
+
Helix: read https://fusion.dev/helix/docs/using-helix-with-an-agent
|
|
186
|
+
before writing components, and https://fusion.dev/helix/custom-elements.json
|
|
187
|
+
for the API.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
| Tool | Where it goes |
|
|
191
|
+
|---|---|
|
|
192
|
+
| Claude Code | `CLAUDE.md`, or a skill in `.claude/skills/` |
|
|
193
|
+
| Cursor | `.cursor/rules/` |
|
|
194
|
+
| GitHub Copilot | `.github/copilot-instructions.md` |
|
|
195
|
+
| Anything else | Whatever it reads. The line is the same |
|
|
196
|
+
|
|
197
|
+
**We do not write those files for you**, and that is the Sovereignty Constraint
|
|
198
|
+
rather than laziness: a file we generate into your repository is one you then
|
|
199
|
+
maintain against our changes, and a tool that rewrites it on install is a
|
|
200
|
+
dependency you did not ask for in a directory you own. The line above is yours to
|
|
201
|
+
paste and yours to delete.
|
|
202
|
+
|
|
203
|
+
## The one thing worth installing
|
|
204
|
+
|
|
205
|
+
```json
|
|
206
|
+
{
|
|
207
|
+
"mcpServers": {
|
|
208
|
+
"helix": { "command": "helix", "args": ["mcp"] }
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`helix mcp` is a subcommand of the CLI, speaking MCP over stdio, which Claude,
|
|
214
|
+
Cursor, Windsurf and Zed all read. It answers the question no document can,
|
|
215
|
+
which is what a given brand color derives to. It runs on your machine, talks to
|
|
216
|
+
a client you chose, and has no network in it.
|
|
217
|
+
|
|
218
|
+
## What this page will not tell you
|
|
219
|
+
|
|
220
|
+
**Anything the manifest answers.** Property names, unions, part names, event
|
|
221
|
+
names and hooks change with the library and are in a file you can fetch; a list
|
|
222
|
+
of them here would be a second copy going stale. Fetch the family.
|
|
223
|
+
|
|
224
|
+
**What a brand color derives to.** That is computation rather than a document,
|
|
225
|
+
and `helix mcp` is where it lives: an MCP server over stdio with
|
|
226
|
+
`helix_audit_theme` and `helix_derive_theme`, both taking the JSON a
|
|
227
|
+
`helix.theme.json` holds. It runs on your machine and nobody hosts it.
|
|
228
|
+
|
|
229
|
+
**Whether a component is good for your case.** The examples are the honest
|
|
230
|
+
answer to that and they are text: every family's `.md` carries them, and
|
|
231
|
+
`https://fusion.dev/helix/sandbox?e=<family>.<slug>` opens one editable.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Binding events, registering elements, and the traps in both.
|
|
3
|
+
pinned: true
|
|
4
|
+
section: Essentials
|
|
5
|
+
title: Using the elements
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Using the elements
|
|
9
|
+
|
|
10
|
+
Write `<helix-card>` in your JSX. They are plain custom elements: no wrapper, no
|
|
11
|
+
patched React, and no React package: there is not one.
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
export default function Page() {
|
|
15
|
+
return (
|
|
16
|
+
<helix-card>
|
|
17
|
+
<h2 slot="header">Deployments</h2>
|
|
18
|
+
<helix-button emphasis="solid" tone="primary">Ship it</helix-button>
|
|
19
|
+
</helix-card>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Everything below is a trap that has actually cost time, rather than general
|
|
25
|
+
advice.
|
|
26
|
+
|
|
27
|
+
## Bind Helix events with `onhelix-*`
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
<helix-tabs onhelix-before-activate={(event) => setPane(event.detail.value)} />
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
There is a real bug underneath this, and the elements work around it for you.
|
|
34
|
+
**React attaches no custom element listener while hydrating**, so on a
|
|
35
|
+
server-rendered page every one of these props is dead until React commits an
|
|
36
|
+
update for some other reason, at which point it silently starts working. Nothing
|
|
37
|
+
is logged. It is
|
|
38
|
+
[facebook/react#35446](https://github.com/facebook/react/issues/35446), open and
|
|
39
|
+
unfixed.
|
|
40
|
+
|
|
41
|
+
Every Helix element binds the handler itself, immediately before it announces
|
|
42
|
+
anything, using the function React is already holding for it. Measured in Chrome
|
|
43
|
+
against React 19.2.8: on a hydrated page the first event reaches the handler,
|
|
44
|
+
and it is never delivered twice once React binds its own listener.
|
|
45
|
+
|
|
46
|
+
Nothing is required of you, and nothing changes once React fixes it.
|
|
47
|
+
|
|
48
|
+
React's own events are unaffected where they bubble. `onClick` on a
|
|
49
|
+
`<helix-button>` goes through the synthetic system, which hydration does set up.
|
|
50
|
+
|
|
51
|
+
A few of ours are native events relayed from inside the component, and the ones
|
|
52
|
+
that do not bubble (`load` and `error` on Avatar, `ended`, `play` and `pause` on
|
|
53
|
+
Video) reach nothing React sets up. The elements bind those for you, so
|
|
54
|
+
`onError` on a `<helix-avatar>` works like the rest.
|
|
55
|
+
|
|
56
|
+
## The prop is `on` plus the event name
|
|
57
|
+
|
|
58
|
+
React slices `on` off the prop and listens for exactly what is left, without
|
|
59
|
+
inspecting it.
|
|
60
|
+
|
|
61
|
+
- `onhelix-before-open` reaches `helix-before-open`. ✅
|
|
62
|
+
- `onhelix-sample-start` reaches `helix-sample-start`. ✅
|
|
63
|
+
- `onHelixOpen` binds an event called `HelixOpen`, which nobody dispatches. ❌
|
|
64
|
+
|
|
65
|
+
The number of hyphens does not matter, measured in Chrome against React 19.2.8.
|
|
66
|
+
An earlier version of this page said two were a hard stop; that was the
|
|
67
|
+
hydration bug above, read as a fact about names.
|
|
68
|
+
|
|
69
|
+
## Register once, in a client component
|
|
70
|
+
|
|
71
|
+
A custom element registers as a side effect of importing its module, and that
|
|
72
|
+
import has to be in the client graph to ever run. A server component's imports
|
|
73
|
+
do not ship to the browser. So one client component holds every element import
|
|
74
|
+
and renders `null`:
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
'use client'
|
|
78
|
+
import '@fusion.dev/helix/card'
|
|
79
|
+
import '@fusion.dev/helix/button'
|
|
80
|
+
|
|
81
|
+
export function HelixElements(): null {
|
|
82
|
+
return null
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Render it once in your layout. **Your pages stay server components.** An
|
|
87
|
+
unregistered tag is just an unknown element in the HTML that upgrades when the
|
|
88
|
+
definition lands. Asking you to mark every page `'use client'` to use a button
|
|
89
|
+
would not be a design system.
|
|
90
|
+
|
|
91
|
+
## Import `elements.css` beside the base layer
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
import '@fusion.dev/helix-styles/base.css'
|
|
95
|
+
import '@fusion.dev/helix/elements.css'
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It hides each tag with `:not(:defined)` until it upgrades, using `visibility` so
|
|
99
|
+
the page does not reflow. Without it a card arrives as a run of unstyled text
|
|
100
|
+
and then snaps into a card.
|
|
101
|
+
|
|
102
|
+
It is generated from the element sources and enumerates the tags, because CSS
|
|
103
|
+
cannot select "any custom element" and a bare `:not(:defined)` would also catch
|
|
104
|
+
your framework's dev overlay.
|
|
105
|
+
|
|
106
|
+
## Import from subpaths
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
import '@fusion.dev/helix/card' // ✅ one family
|
|
110
|
+
import '@fusion.dev/helix' // ❌ the barrel
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
One family per module, so you are not shipping the library to use a button.
|
|
114
|
+
|
|
115
|
+
## There is no declarative shadow DOM
|
|
116
|
+
|
|
117
|
+
Components render on the server through Lit SSR in our own tests, but the app
|
|
118
|
+
ships no DSD, and the shadow styles arrive with the definition rather than in
|
|
119
|
+
the markup. You do not need to configure anything for this; it is here so the
|
|
120
|
+
absence is not mistaken for something missing.
|
|
121
|
+
|
|
122
|
+
The short version of why: DSD cannot activate for script-inserted nodes, a
|
|
123
|
+
server component's output is replayed from the flight payload through DOM APIs
|
|
124
|
+
rather than re-run, and the patch that made it work hooked webpack, which
|
|
125
|
+
Turbopack ignores, silently.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: What a version number means here, and which packages move together.
|
|
3
|
+
order: 6
|
|
4
|
+
section: Getting started
|
|
5
|
+
title: Versioning
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Versioning
|
|
9
|
+
|
|
10
|
+
**Every Helix package shares one version.** `@fusion.dev/helix@1.4.0` works with
|
|
11
|
+
`@fusion.dev/helix-headless@1.4.0` and with `@fusion.dev/helix-styles@1.4.0`, and that is a
|
|
12
|
+
guarantee rather than a coincidence: they are released together, always, from
|
|
13
|
+
one tag.
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
bun add @fusion.dev/helix@1.4.0 @fusion.dev/helix-styles@1.4.0
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Pin them to the same number or pin none of them. A range that lets one drift
|
|
20
|
+
ahead of another is the one configuration this scheme does not defend against.
|
|
21
|
+
|
|
22
|
+
## Why lockstep
|
|
23
|
+
|
|
24
|
+
Cortex moves independently. Full-repo lockstep would be wrong here because the
|
|
25
|
+
documentation packages carry the version of what they document: publishing a
|
|
26
|
+
Cortex fix would bump `@fusion.dev/helix-docs` to a version in which nothing about Helix
|
|
27
|
+
changed.
|
|
28
|
+
|
|
29
|
+
Per-domain lockstep keeps the property that matters, which is that two Helix
|
|
30
|
+
packages of the same version are the pair that was tested together.
|
|
31
|
+
|
|
32
|
+
## The version lives in the tag
|
|
33
|
+
|
|
34
|
+
Every manifest in the repository holds `0.0.0-dev`. The real number is stamped
|
|
35
|
+
in CI, seconds before packing.
|
|
36
|
+
|
|
37
|
+
If you read the source: a manifest saying `0.0.0-dev` is not an unreleased
|
|
38
|
+
package, it is every package. Committing the number back
|
|
39
|
+
would need a bot whose push access outranks branch protection, to record
|
|
40
|
+
something `git tag` already holds.
|
|
41
|
+
|
|
42
|
+
Two tag shapes, and the second is the useful one:
|
|
43
|
+
|
|
44
|
+
| Tag | Means |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `helix-v1.4.0` | A release that promoted |
|
|
47
|
+
| `helix-attempt-v1.4.0` | A number that was spent and did not ship |
|
|
48
|
+
|
|
49
|
+
Version computation reads both, so a failed release burns a number rather than
|
|
50
|
+
poisoning the next one. If you see 1.4.0 skipped, that is what happened.
|
|
51
|
+
|
|
52
|
+
## Documentation is versioned with the code
|
|
53
|
+
|
|
54
|
+
`@fusion.dev/helix-docs@1.4.0` documents Helix 1.4.0. There is no version axis inside
|
|
55
|
+
the package and no version switcher in it: the package version *is* the version,
|
|
56
|
+
because every package in the domain moves together.
|
|
57
|
+
|
|
58
|
+
A site serving several versions installs several versions, which is the
|
|
59
|
+
mechanism npm already provides and the only one that cannot drift.
|
|
60
|
+
|
|
61
|
+
## Stability
|
|
62
|
+
|
|
63
|
+
Components carry a `status` in the manifest and on their page: `experimental`,
|
|
64
|
+
`stable`, or `deprecated`. Most of the library is experimental today and says
|
|
65
|
+
so. That label is the honest one and it will not be softened to make the set
|
|
66
|
+
look more finished than it is.
|
|
67
|
+
|
|
68
|
+
## Release notes
|
|
69
|
+
|
|
70
|
+
On GitHub, generated from the tags. There is no changelog file in this package,
|
|
71
|
+
because a second copy of the release notes is a second thing to keep in step and
|
|
72
|
+
the one that goes stale is the one nobody is reading that week.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Four words with fixed vocabularies, one escape hatch, and the word that is refused.
|
|
3
|
+
section: Theming
|
|
4
|
+
title: Visual axes
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Visual axes
|
|
8
|
+
|
|
9
|
+
A component's visual choices come from four registered axes with fixed
|
|
10
|
+
vocabularies. You cannot invent a value, and a component may take a subset.
|
|
11
|
+
|
|
12
|
+
| Axis | Values |
|
|
13
|
+
|---|---|
|
|
14
|
+
| `emphasis` | `ghost`, `outlined`, `solid`, `subtle` |
|
|
15
|
+
| `material` | `frosted`, `glass`, `transparent` |
|
|
16
|
+
| `separation` | `elevated`, `filled`, `outlined`, `plain` |
|
|
17
|
+
| `tone` | The status and intent words: `primary`, `accent`, `neutral`, `danger`, and so on |
|
|
18
|
+
| `width` | `auto`, `full`, `standard` |
|
|
19
|
+
|
|
20
|
+
```html
|
|
21
|
+
<helix-button
|
|
22
|
+
emphasis="solid"
|
|
23
|
+
tone="primary"
|
|
24
|
+
>Save</helix-button>
|
|
25
|
+
<helix-card separation="outlined">…</helix-card>
|
|
26
|
+
<helix-megamenu material="glass">…</helix-megamenu>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Why two axes
|
|
30
|
+
|
|
31
|
+
**The library had four axes and one word for all of them.** Counted across both
|
|
32
|
+
tiers, `appearance` held 21 declarations describing at least four different
|
|
33
|
+
questions: where a surface ends (`outlined` against `plain` on a Card), how much
|
|
34
|
+
ink a control claims (`ghost` against `solid` on a Button), which widget gets
|
|
35
|
+
rendered (`segmented` against `underlined` on Tabs), and in one case a plain
|
|
36
|
+
misfiled tone.
|
|
37
|
+
|
|
38
|
+
The tell is that `outlined` appeared in two of those groups meaning different
|
|
39
|
+
things. On a Card it says where the surface ends. On a Button it says how much
|
|
40
|
+
ink. Same word, two questions, and no way to ask one without answering the
|
|
41
|
+
other.
|
|
42
|
+
|
|
43
|
+
**`appearance` is refused outright now.** It is not a word this library uses,
|
|
44
|
+
and a component reintroducing it fails the build.
|
|
45
|
+
|
|
46
|
+
## Two axes, not one
|
|
47
|
+
|
|
48
|
+
This is the test, and the library found it before the registry existed.
|
|
49
|
+
`megamenu-root.css` reads: a consumer can make the panel glass, which is not an
|
|
50
|
+
appearance, because `full` already occupies that axis with a layout. A panel can
|
|
51
|
+
be glass *and* outlined. Two axes.
|
|
52
|
+
|
|
53
|
+
If you are adding a property and cannot decide which axis it belongs to, that is
|
|
54
|
+
usually the answer: it is two properties.
|
|
55
|
+
|
|
56
|
+
## `variant` is the escape hatch
|
|
57
|
+
|
|
58
|
+
```html
|
|
59
|
+
<helix-tabs variant="segmented">…</helix-tabs>
|
|
60
|
+
<helix-time-picker variant="wheels">…</helix-time-picker>
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
`variant` takes no registry, being component-specific by definition: `segmented`
|
|
64
|
+
against `underlined`, `list` against `wheels`, `dots` against `marks`. It is a
|
|
65
|
+
choice between two widgets, which is structural wherever it is made, so it lives
|
|
66
|
+
on either tier.
|
|
67
|
+
|
|
68
|
+
**What it may not be is a place to put something that is on an axis.** That is
|
|
69
|
+
the failure mode `appearance` demonstrated for 21 components.
|
|
70
|
+
|
|
71
|
+
## `tone` is the one axis both tiers carry
|
|
72
|
+
|
|
73
|
+
A styled axis never appears on the headless tier, which has nothing to paint and
|
|
74
|
+
no box to size. `tone` is the exception and a principled one: it says what a
|
|
75
|
+
thing *means*, and meaning survives having no stylesheet.
|
package/content/vue.mdx
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: The one compiler setting Vue needs, and why props and events already work.
|
|
3
|
+
parent: installation
|
|
4
|
+
section: Getting started
|
|
5
|
+
title: Vue
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Vue
|
|
9
|
+
|
|
10
|
+
```ts
|
|
11
|
+
import '@fusion.dev/helix/vue'
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
A types-only import, once. It augments Vue's `GlobalComponents` so every Helix
|
|
15
|
+
tag is typed in an SFC template, with its real props and its real events. The
|
|
16
|
+
generated file declares the augmentation under both `vue` and
|
|
17
|
+
`@vue/runtime-core`, because tooling disagrees about which one to read.
|
|
18
|
+
|
|
19
|
+
## Tell the compiler the tags are deliberate
|
|
20
|
+
|
|
21
|
+
Without this, Vue resolves `<helix-button>` as a component it cannot find and
|
|
22
|
+
warns on every render.
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
// vite.config.ts
|
|
26
|
+
export default defineConfig({
|
|
27
|
+
plugins: [
|
|
28
|
+
vue({
|
|
29
|
+
template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith('helix-') } },
|
|
30
|
+
}),
|
|
31
|
+
],
|
|
32
|
+
})
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The prefix test is the whole rule. Every element in the library is `helix-`
|
|
36
|
+
something, in both tiers, and nothing else is.
|
|
37
|
+
|
|
38
|
+
## Props and events need no ceremony
|
|
39
|
+
|
|
40
|
+
Vue sets a property when the element has one and falls back to an attribute
|
|
41
|
+
otherwise, which is exactly right for these: a string goes through either path
|
|
42
|
+
and an array or an object goes through the property.
|
|
43
|
+
|
|
44
|
+
```vue
|
|
45
|
+
<helix-tabs :items="items" @helix-before-activate="onActivate" />
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`@helix-before-activate` binds with `addEventListener`, so the name is the event's own.
|
|
49
|
+
Vue is the framework this works most quietly in, which is why this page is
|
|
50
|
+
short: there is no bridge and no wrapper, only the compiler setting above.
|
|
51
|
+
|
|
52
|
+
**Force the property with `.prop` if you ever need to**, for instance where a
|
|
53
|
+
string would be ambiguous:
|
|
54
|
+
|
|
55
|
+
```vue
|
|
56
|
+
<helix-input :value.prop="'42'" />
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Rarely necessary, and it exists so you do not go looking for it.
|
|
60
|
+
|
|
61
|
+
## Seeding, mirroring and refusing
|
|
62
|
+
|
|
63
|
+
`value`, `open`, `checked` and `active` are seeds. The element applies its own
|
|
64
|
+
changes from there, so all three of these work. See [State and
|
|
65
|
+
control](./state-and-control).
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<!-- Seeded: the element owns it from there. -->
|
|
69
|
+
<helix-input default-value="Ada" label="Name" />
|
|
70
|
+
|
|
71
|
+
<!-- Mirrored: bind the notification and let the element apply. -->
|
|
72
|
+
<helix-input label="Name" @change="name = $event.target.value" />
|
|
73
|
+
|
|
74
|
+
<!-- Refused: you take the decision for this one change. -->
|
|
75
|
+
<helix-input
|
|
76
|
+
:value="name"
|
|
77
|
+
label="Name"
|
|
78
|
+
@helix-before-change="$event.preventDefault(); name = clean($event.detail.value)"
|
|
79
|
+
/>
|
|
80
|
+
```
|