@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,170 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Real form participation, labels that resolve across a shadow root, and the disabled trap.
|
|
3
|
+
section: Essentials
|
|
4
|
+
title: Forms and validation
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Forms and validation
|
|
8
|
+
|
|
9
|
+
Twenty-two families are form-associated custom elements, so a `<form>` collects
|
|
10
|
+
them, resets them, restores them on back-navigation and validates them the way it
|
|
11
|
+
does a native control. Nothing about that is simulated.
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<form>
|
|
15
|
+
<helix-input
|
|
16
|
+
label="Name"
|
|
17
|
+
name="name"
|
|
18
|
+
required
|
|
19
|
+
></helix-input>
|
|
20
|
+
<helix-select
|
|
21
|
+
label="Plan"
|
|
22
|
+
name="plan"
|
|
23
|
+
>
|
|
24
|
+
<helix-option value="free">Free</helix-option>
|
|
25
|
+
<helix-option value="pro">Pro</helix-option>
|
|
26
|
+
</helix-select>
|
|
27
|
+
<helix-button type="submit">Save</helix-button>
|
|
28
|
+
</form>
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`new FormData(form)` has `name` and `plan` in it. So does a server reading the
|
|
32
|
+
POST body.
|
|
33
|
+
|
|
34
|
+
## Every control carries its own label
|
|
35
|
+
|
|
36
|
+
```html
|
|
37
|
+
<helix-input
|
|
38
|
+
hint="We only use this to sign you in."
|
|
39
|
+
label="Email"
|
|
40
|
+
name="email"
|
|
41
|
+
required
|
|
42
|
+
></helix-input>
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
There is no wrapping `Field` component, and that is not a style preference. **A
|
|
46
|
+
wrapper cannot name what it wraps.** Its label would live in one shadow root and
|
|
47
|
+
the control in another, and measured in Chrome 148: an `aria-labelledby` IDREF
|
|
48
|
+
cannot see across a shadow boundary, and `control.ariaLabelledByElements =
|
|
49
|
+
[labelInTheWrapper]` reads back as zero elements, the reference silently
|
|
50
|
+
dropped. The same assignment with a label in the control's own tree is kept.
|
|
51
|
+
|
|
52
|
+
That pattern works in React component libraries because they have no shadow
|
|
53
|
+
boundary. Bringing it here brings the ergonomics and none of the conditions that
|
|
54
|
+
made it work.
|
|
55
|
+
|
|
56
|
+
So every control that wants a label has the same three properties and renders
|
|
57
|
+
its own nodes:
|
|
58
|
+
|
|
59
|
+
| Property | For |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `label` | The accessible name, and the fallback for the `label` slot |
|
|
62
|
+
| `hint` | Help under the control, while there is nothing wrong |
|
|
63
|
+
| `feedback` | What is wrong, in your words rather than the browser's |
|
|
64
|
+
|
|
65
|
+
Each is a slot fallback, so anything richer than a string replaces it:
|
|
66
|
+
|
|
67
|
+
```html
|
|
68
|
+
<helix-input name="email">
|
|
69
|
+
<span slot="label">Email <helix-badge>required</helix-badge></span>
|
|
70
|
+
</helix-input>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
**A whitespace-only text node defeats a default slot's fallback**, which costs a
|
|
74
|
+
label and gives no warning. Measured in Chrome 148: a newline between the tags
|
|
75
|
+
is enough, so `<helix-checkbox label="Accept">` closed on the next line renders
|
|
76
|
+
no label at all. Named slots do not have this problem, because `slot="label"` is
|
|
77
|
+
an attribute and a text node has none. It is the platform's behavior rather
|
|
78
|
+
than ours, and it is true of every slot fallback in the library.
|
|
79
|
+
|
|
80
|
+
## Validation
|
|
81
|
+
|
|
82
|
+
The platform's own validation works, because these are real form participants:
|
|
83
|
+
|
|
84
|
+
```html
|
|
85
|
+
<helix-input
|
|
86
|
+
label="Email"
|
|
87
|
+
name="email"
|
|
88
|
+
required
|
|
89
|
+
type="email"
|
|
90
|
+
></helix-input>
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Submitting an empty form blocks and points at the control. `checkValidity()`,
|
|
94
|
+
`reportValidity()` and `:invalid` behave.
|
|
95
|
+
|
|
96
|
+
**`feedback` overrides the message.** Left empty, the browser's own is shown,
|
|
97
|
+
which is localised and familiar; set, it says what you want in the words your
|
|
98
|
+
product uses:
|
|
99
|
+
|
|
100
|
+
```html
|
|
101
|
+
<helix-input
|
|
102
|
+
feedback="We need this to send your receipt."
|
|
103
|
+
label="Email"
|
|
104
|
+
name="email"
|
|
105
|
+
required
|
|
106
|
+
></helix-input>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**A group is the participant, not its members.** `Radio.Group` and
|
|
110
|
+
`Toggle.Group` own the `name`, the `value` and the validity: a lone radio is not
|
|
111
|
+
a control at all, it is an answer to one. `Checkbox.Group` is the exception in
|
|
112
|
+
the other direction, since each box in it submits itself already, so the group
|
|
113
|
+
deliberately reports no value of its own.
|
|
114
|
+
|
|
115
|
+
## The disabled trap
|
|
116
|
+
|
|
117
|
+
```html
|
|
118
|
+
<fieldset disabled>
|
|
119
|
+
<helix-input
|
|
120
|
+
label="Name"
|
|
121
|
+
name="name"
|
|
122
|
+
></helix-input>
|
|
123
|
+
</fieldset>
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
That works, and it did not always. The platform tells a form-associated element
|
|
127
|
+
through `formDisabledCallback`, which nothing implemented, so a control inside a
|
|
128
|
+
disabled fieldset stayed fully typeable and announced itself as enabled.
|
|
129
|
+
Measured: a `helix-input` in one accepted text, reported `disabled` as false,
|
|
130
|
+
and carried no `aria-disabled`, while the form's entry list correctly left it
|
|
131
|
+
out. Interactive and unsubmittable is the worst of both.
|
|
132
|
+
|
|
133
|
+
**For your own CSS this means one rule.** Style disabled controls with
|
|
134
|
+
`:disabled`, never `[disabled]`:
|
|
135
|
+
|
|
136
|
+
```css
|
|
137
|
+
/* Right: matches the attribute and the ancestor fieldset. */
|
|
138
|
+
helix-input:disabled { … }
|
|
139
|
+
|
|
140
|
+
/* Wrong: misses every control inside a disabled fieldset. */
|
|
141
|
+
helix-input[disabled] { … }
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
The pseudo-class is true for a control carrying the attribute *and* for one
|
|
145
|
+
inside a disabled fieldset, which sets no attribute on anything. A component
|
|
146
|
+
styled with the attribute selector refuses interaction from the fieldset,
|
|
147
|
+
because the platform disables it either way, and paints itself as live. That
|
|
148
|
+
reads as a control that is simply broken, and it is invisible in every example
|
|
149
|
+
that does not happen to use a fieldset. The library holds itself to this, and
|
|
150
|
+
the same reasoning applies to yours.
|
|
151
|
+
|
|
152
|
+
`disabled` is mirrored to `aria-disabled` rather than removing the control from
|
|
153
|
+
the accessibility tree, so a screen reader can still announce that the control
|
|
154
|
+
exists and is currently off.
|
|
155
|
+
|
|
156
|
+
## Seeding a form
|
|
157
|
+
|
|
158
|
+
A declared `value` or `checked` is a seed and the control goes on managing
|
|
159
|
+
itself, so a form needs nothing wired to work. `default-value` is what a reset
|
|
160
|
+
returns to, which is usually what a seed in a form means:
|
|
161
|
+
|
|
162
|
+
```html
|
|
163
|
+
<helix-input
|
|
164
|
+
default-value="Ada"
|
|
165
|
+
label="Name"
|
|
166
|
+
name="name"
|
|
167
|
+
></helix-input>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
See [State and control](./state-and-control).
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: 103 glyphs, one geometry, and how to draw one that is not in the set.
|
|
3
|
+
section: Foundations
|
|
4
|
+
title: Iconography
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Iconography
|
|
8
|
+
|
|
9
|
+
```html
|
|
10
|
+
<helix-icon name="trash"></helix-icon>
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
103 glyphs, from Phosphor, drawn on a 256 box at stroke-width 16. That is
|
|
14
|
+
Phosphor's own geometry and it is kept rather than rescaled: normalizing to a 24
|
|
15
|
+
box would change the optical weight of every glyph and stop the set matching
|
|
16
|
+
anything upstream.
|
|
17
|
+
|
|
18
|
+
## Size and color come from the context
|
|
19
|
+
|
|
20
|
+
An icon has no `size` and no `color` property. It is `1em` square and
|
|
21
|
+
`currentColor`, so it takes the size and color of the text it sits in:
|
|
22
|
+
|
|
23
|
+
```html
|
|
24
|
+
<span data-tone="danger">
|
|
25
|
+
<helix-icon name="warning"></helix-icon> Careful
|
|
26
|
+
</span>
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Inside a control, `--helix-size-icon` is what decides, and it follows the
|
|
30
|
+
region's density along with everything else. That is why an icon in a small
|
|
31
|
+
button is smaller without anybody saying so.
|
|
32
|
+
|
|
33
|
+
## Decorative or named
|
|
34
|
+
|
|
35
|
+
```html
|
|
36
|
+
<!-- Decorative: the text beside it already says what this is. -->
|
|
37
|
+
<helix-button>
|
|
38
|
+
<helix-icon name="trash"></helix-icon>
|
|
39
|
+
Delete
|
|
40
|
+
</helix-button>
|
|
41
|
+
|
|
42
|
+
<!-- Named: there is no text, so the icon has to carry the name. -->
|
|
43
|
+
<helix-icon-button
|
|
44
|
+
label="Delete"
|
|
45
|
+
name="trash"
|
|
46
|
+
></helix-icon-button>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
An icon with no `label` is hidden from assistive technology, which is right when
|
|
50
|
+
the words are beside it and wrong when they are not. `Icon.Button` takes a
|
|
51
|
+
`label` because it never has words beside it.
|
|
52
|
+
|
|
53
|
+
## Importing a glyph directly
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { trash } from '@fusion.dev/helix-icons'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Each glyph is an exported string, so you can put one in your own markup, in a
|
|
60
|
+
canvas, or anywhere an element is not what you want. Every `name` the library
|
|
61
|
+
uses is held to the set of glyphs that actually exist, so a typo is a build
|
|
62
|
+
failure rather than an empty box.
|
|
63
|
+
|
|
64
|
+
## Adding one
|
|
65
|
+
|
|
66
|
+
Drop the SVG into `helix/icons/raw` and run `bun run icons`. The set is
|
|
67
|
+
generated from those files, so adding a glyph is adding a file rather than
|
|
68
|
+
hand-transcribing a path into a TypeScript object.
|
|
69
|
+
|
|
70
|
+
Keep to the same geometry: 256 box, stroke-width 16, no fill. A glyph drawn at a
|
|
71
|
+
different weight is visible immediately when it sits beside one that was not.
|
|
72
|
+
|
|
73
|
+
## Icons this set does not have
|
|
74
|
+
|
|
75
|
+
Product marks. Helix and Cortex have marks and they live in the icon package,
|
|
76
|
+
but a mark is not an icon: it identifies a product and does not mean an action.
|
|
77
|
+
Three earlier attempts at product marks were removed for being pictograms of a
|
|
78
|
+
metaphor standing beside a parent that is a geometric monogram, and the rule
|
|
79
|
+
that replaced them is that **a mark is one rounded body with material removed**.
|
|
80
|
+
If you are reaching for the icon set to draw a logo, that is the wrong drawer.
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: What to install, the order the stylesheets go in, and how to check it worked.
|
|
3
|
+
order: 3
|
|
4
|
+
section: Getting started
|
|
5
|
+
title: Installation
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Installation
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
bun add @fusion.dev/helix
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
`@fusion.dev/helix-styles` is not optional and not a peer dependency by accident. The
|
|
15
|
+
components reference semantic tokens and paint nothing without them: a Helix
|
|
16
|
+
page with the elements installed and the tokens missing renders unstyled boxes
|
|
17
|
+
rather than an unthemed version of itself.
|
|
18
|
+
|
|
19
|
+
## The stylesheets go in this order
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
import '@fusion.dev/helix/helix.css'
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
One import, and it is four files in the order they have to be in:
|
|
26
|
+
`primitives.css`, `tokens.css`, `base.css`, `elements.css`. The order is
|
|
27
|
+
load-bearing and the failure is silent, which these instructions demonstrated
|
|
28
|
+
for longer than anyone would like, so it is stated once inside the package
|
|
29
|
+
rather than by every consumer.
|
|
30
|
+
|
|
31
|
+
It resolves `@fusion.dev/helix-styles` through this package's own dependency, so
|
|
32
|
+
installing `@fusion.dev/helix` is enough. A bare specifier in an `@import` is
|
|
33
|
+
resolved by a bundler and never by a browser, so a page serving CSS through a
|
|
34
|
+
`<link>` takes the four by path instead.
|
|
35
|
+
|
|
36
|
+
### The four, for anyone substituting a layer
|
|
37
|
+
|
|
38
|
+
Each file keeps its own export, so a consumer bringing their own reset or their
|
|
39
|
+
own ramp imports the two they want and skips the bundle.
|
|
40
|
+
|
|
41
|
+
- **`primitives.css`** is the raw ramp the semantic layer is derived from, and
|
|
42
|
+
it is where the seven `--helix-seed-*` knobs are read. Nothing in your own
|
|
43
|
+
code should name a primitive, and leaving the file out is not a way to
|
|
44
|
+
enforce that: `tokens.css` refers to one 235 times, so without it every
|
|
45
|
+
semantic token resolves to nothing at all. Measured on a page missing it,
|
|
46
|
+
`--helix-color-background` computes to `rgba(0, 0, 0, 0)`, and so does every
|
|
47
|
+
other color. It is also the file [theming](theming.mdx) depends on, since a
|
|
48
|
+
`--helix-seed-brand` nobody reads moves nothing. **This is the one that was
|
|
49
|
+
missing from this page**, which is why the bundle exists.
|
|
50
|
+
- **`tokens.css`** declares the semantic layer, 177 custom properties in 15
|
|
51
|
+
categories, plus `color-scheme: light dark` on `:root`. Everything below reads
|
|
52
|
+
from it, and it reads primitives.
|
|
53
|
+
- **`base.css`** is the element reset, the type scale on bare `h1` through
|
|
54
|
+
`h6`, and the recipes for markup this library does not render. It reads
|
|
55
|
+
tokens.
|
|
56
|
+
|
|
57
|
+
**The last one is the one people leave out**, because everything works without
|
|
58
|
+
it right up until the page is server-rendered: an element that has not upgraded
|
|
59
|
+
yet has no shadow root and no styles of its own, so the markup flashes unstyled
|
|
60
|
+
and then snaps into place. `elements.css` is what makes that interval look like
|
|
61
|
+
the finished page.
|
|
62
|
+
|
|
63
|
+
Put them in a stylesheet you control rather than deep inside a component, so the
|
|
64
|
+
order is visible in one place and stays that way.
|
|
65
|
+
|
|
66
|
+
## Register the elements
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
import '@fusion.dev/helix'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
A side-effect import. It calls `customElements.define` for every element, once,
|
|
73
|
+
and is safe to run more than once: the helper behind it checks the registry
|
|
74
|
+
first, because a second `define` for the same tag throws and takes the rest of
|
|
75
|
+
the module with it.
|
|
76
|
+
|
|
77
|
+
You can register a subset instead, from a subpath:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
import '@fusion.dev/helix/button'
|
|
81
|
+
import '@fusion.dev/helix/dialog'
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Worth doing when you use a handful of components. Not worth doing in the belief
|
|
85
|
+
that the full import ships everything to the browser: the package is
|
|
86
|
+
side-effectful by design, so a bundler keeps what you import either way, and the
|
|
87
|
+
subpaths are about being explicit rather than about size.
|
|
88
|
+
|
|
89
|
+
## Check it worked
|
|
90
|
+
|
|
91
|
+
Render one element and ask the browser whether it upgraded:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
await customElements.whenDefined('helix-button')
|
|
95
|
+
console.log(document.querySelector('helix-button')?.shadowRoot !== null)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`true` means the definition arrived and the element built its shadow root.
|
|
99
|
+
`false`, or a promise that never settles, means the registration import did not
|
|
100
|
+
run: usually a `'use client'` boundary the import ended up outside of, or a
|
|
101
|
+
subpath import for a component you did not end up using.
|
|
102
|
+
|
|
103
|
+
**A styling problem looks nothing like that.** If the element upgraded and the
|
|
104
|
+
page still looks wrong, the stylesheets are the thing to check, in the order
|
|
105
|
+
above. The tell is which layer is missing: no color at all is `tokens.css`,
|
|
106
|
+
unstyled prose and bare headings is `base.css`, and components that look right
|
|
107
|
+
once JavaScript runs but flash unstyled before it is `elements.css`.
|
|
108
|
+
|
|
109
|
+
## Your framework
|
|
110
|
+
|
|
111
|
+
The elements are custom elements and every framework can render one. What
|
|
112
|
+
differs is how a framework passes a non-string value, how it binds an event, and
|
|
113
|
+
whether it needs telling that an unknown tag is deliberate.
|
|
114
|
+
|
|
115
|
+
| | |
|
|
116
|
+
|---|---|
|
|
117
|
+
| [React](./react) | A runtime bridge ships for it, plus `jsx.d.ts` |
|
|
118
|
+
| [Vue](./vue) | Ships `vue.d.ts`, needs `compilerOptions.isCustomElement` |
|
|
119
|
+
| [Svelte](./svelte) | Ships `svelte.d.ts`, and needs nothing else |
|
|
120
|
+
| [Angular](./angular) | Needs `CUSTOM_ELEMENTS_SCHEMA` |
|
|
121
|
+
| [Other frameworks](./other-frameworks) | Solid, Qwik, Lit, and the three rules they all reduce to |
|
|
122
|
+
| [Without a bundler](./without-a-bundler) | An import map, or the stylesheets alone |
|
|
123
|
+
|
|
124
|
+
A framework gets its own page when Helix ships typings for it, or when it
|
|
125
|
+
refuses to compile without configuration. Everything else works with no setup.
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Every input the theme engine takes, and what each one decides.
|
|
3
|
+
section: Theming
|
|
4
|
+
title: Intent reference
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Intent reference
|
|
8
|
+
|
|
9
|
+
[Deriving a theme](./theming) is the argument. This is the surface: everything
|
|
10
|
+
`ThemeIntent` accepts, and what changes when you set it.
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { derive } from '@fusion.dev/helix-theme'
|
|
14
|
+
|
|
15
|
+
const theme = derive({ brand: '#2563eb' })
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**`brand` is the only required input.** Everything else has a default that is
|
|
19
|
+
derived from it or from the design language, and a theme that sets one field is
|
|
20
|
+
a valid theme.
|
|
21
|
+
|
|
22
|
+
## Color
|
|
23
|
+
|
|
24
|
+
| Field | Decides |
|
|
25
|
+
|---|---|
|
|
26
|
+
| `brand` | The one color most consumers care about. Required |
|
|
27
|
+
| `accent` | The second emphasis. Defaults to the complement, `{ from: 'brand', rotate: 180 }` |
|
|
28
|
+
| `neutral` | The gray ramp, which can be tinted toward the brand |
|
|
29
|
+
| `status` | `info`, `success`, `warning`, `danger`. A color pins one; an object nudges its hue within the meaning it already carries |
|
|
30
|
+
| `chroma` | How saturated the palette is, independent of hue |
|
|
31
|
+
| `scheme` | `'light'` or `'dark'`, for a theme that is only one |
|
|
32
|
+
| `tokens` | Literal overrides, by token name, for the cases nothing else reaches |
|
|
33
|
+
|
|
34
|
+
**`accent` may be a relation and `status` may not**, and the asymmetry is
|
|
35
|
+
deliberate: a `danger` that rotates with the brand is a warning a reader has to
|
|
36
|
+
re-learn per product.
|
|
37
|
+
|
|
38
|
+
**`chroma` is the axis a brand's feel lives on.** Monotone, muted, restrained
|
|
39
|
+
and vivid are all chroma statements and none of them is a hue. `ceiling` clamps,
|
|
40
|
+
so 0 is monotone and roughly 0.10 is muted; `scale` multiplies what the brand
|
|
41
|
+
supplies. Both apply to every derived color including the brand itself, because
|
|
42
|
+
a monotone theme is one where the brand is gray too. Never applied to a value
|
|
43
|
+
you pinned: a pinned color is a fact.
|
|
44
|
+
|
|
45
|
+
## The guarantee
|
|
46
|
+
|
|
47
|
+
| Field | Decides |
|
|
48
|
+
|---|---|
|
|
49
|
+
| `contrastTarget` | Target contrast for body text. 4.5 is WCAG AA, 7 is AAA |
|
|
50
|
+
| `pairings` | Extra foreground and background pairs to solve for, beyond the model's own |
|
|
51
|
+
|
|
52
|
+
4.5 is the default because the European Accessibility Act points at WCAG 2.1 AA,
|
|
53
|
+
so it is the figure with legal weight.
|
|
54
|
+
|
|
55
|
+
## Shape and space
|
|
56
|
+
|
|
57
|
+
| Field | Decides |
|
|
58
|
+
|---|---|
|
|
59
|
+
| `radius` | Corner rounding across the system |
|
|
60
|
+
| `density` | Scales spacing and control heights. 1 is default, 0.875 is compact |
|
|
61
|
+
| `separation` | Whether a surface is told apart by an edge or by a lift |
|
|
62
|
+
|
|
63
|
+
`separation` is the policy pair described in [Surfaces and
|
|
64
|
+
separation](./surfaces-and-separation).
|
|
65
|
+
|
|
66
|
+
## Type and motion
|
|
67
|
+
|
|
68
|
+
| Field | Decides |
|
|
69
|
+
|---|---|
|
|
70
|
+
| `font` | The families, and the half of the type system that is a scale |
|
|
71
|
+
| `motion` | `character` and `scale` |
|
|
72
|
+
|
|
73
|
+
**`motion.character` sets the curve every component reaches for and carries a
|
|
74
|
+
duration multiplier with it**, because the two are one decision: a spring over
|
|
75
|
+
120ms is a twitch, and a soft curve over 120ms is a fade. `scale` multiplies on
|
|
76
|
+
top for a theme that wants the character and not the pace.
|
|
77
|
+
|
|
78
|
+
`instant` is not scaled. It is 0 and means "do not animate", which is a
|
|
79
|
+
statement rather than a duration, and it emits no curves at all: three easings
|
|
80
|
+
for transitions of zero length is a contradiction.
|
|
81
|
+
|
|
82
|
+
`precise` is the one character that is not physical. The other three accelerate,
|
|
83
|
+
settle or overshoot; linear reads as moved by a mechanism, and it is the only
|
|
84
|
+
curve legible on a short travel, because constancy shows in the timing where an
|
|
85
|
+
overshoot shows in the distance.
|
|
86
|
+
|
|
87
|
+
## Material
|
|
88
|
+
|
|
89
|
+
| Field | Decides |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `material` | `blur` in pixels and `opacity` from 0 to 1 |
|
|
92
|
+
|
|
93
|
+
**This is the one choice that can invalidate the contrast guarantee.** Contrast
|
|
94
|
+
is measured against a known ground, and a translucent surface does not have one,
|
|
95
|
+
so a pair over it comes back `unverifiable` rather than measured. That is
|
|
96
|
+
reported rather than hidden: a theme with heavy material is making a trade and
|
|
97
|
+
should be able to see it.
|
|
98
|
+
|
|
99
|
+
## Output
|
|
100
|
+
|
|
101
|
+
| Field | Decides |
|
|
102
|
+
|---|---|
|
|
103
|
+
| `namespace` | The token prefix. Defaults to `helix` |
|
|
104
|
+
|
|
105
|
+
`namespace` is for a product shipping its own token layer built on this engine.
|
|
106
|
+
Everything else in the library reads `--helix-*`, so changing it is a decision
|
|
107
|
+
about your own surface rather than about Helix's.
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: What Helix is, and the four packages it stacks into.
|
|
3
|
+
order: 1
|
|
4
|
+
section: Getting started
|
|
5
|
+
title: Introduction
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Helix
|
|
9
|
+
|
|
10
|
+
A design system of web components: tokens, a headless component tier, a styled
|
|
11
|
+
tier, and the framework typings generated from both. It works in any framework,
|
|
12
|
+
or none, because the components are custom elements.
|
|
13
|
+
|
|
14
|
+
Two things make it different from a component library.
|
|
15
|
+
|
|
16
|
+
**Theming is derivation, not override.** You supply a handful of intent inputs
|
|
17
|
+
and the system computes a complete token set whose every foreground and
|
|
18
|
+
background pair meets its contrast target by construction. See
|
|
19
|
+
[Theming](./theming).
|
|
20
|
+
|
|
21
|
+
**AI is a first-class consumer.** Components are described by a custom elements
|
|
22
|
+
manifest with typed enums, documented parts, and generated React prop names. An
|
|
23
|
+
agent can write correct Helix code by reading types alone. That is why
|
|
24
|
+
conventions like inline literal unions and documented CSS parts are enforced at
|
|
25
|
+
analysis time rather than left to review.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
bun add @fusion.dev/helix @fusion.dev/helix-styles
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Three imports at your app root, in this order, then the elements are usable
|
|
34
|
+
anywhere:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import '@fusion.dev/helix/helix.css'
|
|
38
|
+
import '@fusion.dev/helix'
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<helix-accordion separation="outlined">
|
|
43
|
+
<helix-accordion-item default-open>
|
|
44
|
+
<span slot="summary">What is Helix?</span>
|
|
45
|
+
<p>A design system that derives its own accessible palette.</p>
|
|
46
|
+
</helix-accordion-item>
|
|
47
|
+
</helix-accordion>
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
[Installation](./installation) has the order those imports are in and why, and a
|
|
51
|
+
page for each framework.
|
|
52
|
+
|
|
53
|
+
## Packages
|
|
54
|
+
|
|
55
|
+
| Package | Use when |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `@fusion.dev/helix-styles` | Always. Tokens and the base layer |
|
|
58
|
+
| `@fusion.dev/helix` | You want Helix components as-is |
|
|
59
|
+
| `@fusion.dev/helix-headless` | You want the behavior and none of the styling |
|
|
60
|
+
| `@fusion.dev/helix-theme` | You are generating or validating a theme |
|
|
61
|
+
| `@fusion.dev/helix-icons` | You are drawing an icon outside a component |
|
|
62
|
+
| `@fusion.dev/helix-core` | You are building a new Helix component |
|
|
63
|
+
|
|
64
|
+
**There is no React package.** The elements are custom elements and React
|
|
65
|
+
renders them directly; what ships for React is a `jsx.d.ts` that types every tag
|
|
66
|
+
and a runtime bridge in `@fusion.dev/helix-core` that repairs React's hydration of
|
|
67
|
+
custom events. [React](./react) has both.
|
|
68
|
+
|
|
69
|
+
See [Tiers](./tiers) for how the packages relate and which one to reach for.
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Locale-aware controls, translatable strings, and a layout that already flips.
|
|
3
|
+
section: Essentials
|
|
4
|
+
title: Localization and RTL
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Localization and RTL
|
|
8
|
+
|
|
9
|
+
## Right-to-left already works
|
|
10
|
+
|
|
11
|
+
Set `dir` and the layout flips:
|
|
12
|
+
|
|
13
|
+
```html
|
|
14
|
+
<html
|
|
15
|
+
dir="rtl"
|
|
16
|
+
lang="ar"
|
|
17
|
+
>
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Every component is written in logical properties, `inline-start` rather than
|
|
21
|
+
`left`, `margin-block` rather than `margin-top`. Sixty-seven stylesheets name a
|
|
22
|
+
direction, and **not one of them names a physical side**: there is no
|
|
23
|
+
`margin-left`, no `padding-right` and no `inset-top` anywhere in the library.
|
|
24
|
+
Nothing infers the direction for you and nothing needs to: the document says it
|
|
25
|
+
and the components follow.
|
|
26
|
+
|
|
27
|
+
The same is true of anything you write on top. Use logical properties and your
|
|
28
|
+
layout flips with ours; use `left` and `right` and it will not.
|
|
29
|
+
|
|
30
|
+
## Locale-aware controls
|
|
31
|
+
|
|
32
|
+
Six families format numbers or dates and take a `locale`:
|
|
33
|
+
|
|
34
|
+
| | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `Date Input`, `Date Picker` | Date formats and first day of week |
|
|
37
|
+
| `Time Input`, `Time Picker` | 12 or 24 hour, and separators |
|
|
38
|
+
| `Pagination` | Grouped thousands in page numbers |
|
|
39
|
+
| `File Input` | File sizes |
|
|
40
|
+
|
|
41
|
+
```html
|
|
42
|
+
<helix-pagination
|
|
43
|
+
locale="de-DE"
|
|
44
|
+
pages="1200"
|
|
45
|
+
></helix-pagination>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Left unset, they use the browser's own locale, which is usually right. Set it
|
|
49
|
+
when your application's language is a choice the reader made rather than the one
|
|
50
|
+
their operating system is in.
|
|
51
|
+
|
|
52
|
+
Formatting goes through `Intl`, so there is no locale data bundled and nothing
|
|
53
|
+
to keep up to date.
|
|
54
|
+
|
|
55
|
+
## Translatable strings
|
|
56
|
+
|
|
57
|
+
Seventy properties across the headless tier are strings a reader sees, and every
|
|
58
|
+
one is a property rather than a hardcoded word. The ones that combine
|
|
59
|
+
values are format templates:
|
|
60
|
+
|
|
61
|
+
```html
|
|
62
|
+
<helix-pagination-summary
|
|
63
|
+
format="{from} bis {to} von {total}"
|
|
64
|
+
locale="de-DE"
|
|
65
|
+
page="2"
|
|
66
|
+
page-size="10"
|
|
67
|
+
total="1500"
|
|
68
|
+
></helix-pagination-summary>
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
The placeholders are named rather than positional, so a translation can reorder
|
|
72
|
+
them, which some languages require and a positional format cannot express.
|
|
73
|
+
|
|
74
|
+
**Pass these from your own translation system.** Helix ships English defaults
|
|
75
|
+
and no translation machinery: a design system that owned the string catalog
|
|
76
|
+
would be a second one to keep in step with yours.
|
|
77
|
+
|
|
78
|
+
## What to check when adding a language
|
|
79
|
+
|
|
80
|
+
- **Long strings.** German and Finnish will find every control that assumed a
|
|
81
|
+
short label. Controls take their width from the layout rather than carrying
|
|
82
|
+
one, which helps, but a toolbar that fitted six buttons may now fit four.
|
|
83
|
+
- **The `lang` attribute, not just the strings.** Hyphenation, quotation marks
|
|
84
|
+
and font fallback all key off it.
|
|
85
|
+
- **Numerals.** `Intl` handles the digits; a hardcoded `%` or a currency symbol
|
|
86
|
+
in your own markup will not move to the right side.
|
|
87
|
+
- **Icons that point.** A chevron meaning "forward" is mirrored by the logical
|
|
88
|
+
properties; one baked into an SVG path is not.
|
|
89
|
+
|
|
90
|
+
## What is not covered
|
|
91
|
+
|
|
92
|
+
There is no message catalog, no pluralization helper and no date-range
|
|
93
|
+
formatter beyond what the controls do for themselves. Those belong to your
|
|
94
|
+
application's i18n layer, and every one of these properties is designed to be
|
|
95
|
+
fed from it.
|