@fusion.dev/helix-templates 0.138.0 → 0.140.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.
- package/README.md +18 -6
- package/dist/index.js +24 -0
- package/package.json +1 -1
- package/templates/angular/angular.json +57 -0
- package/templates/angular/package.json +28 -0
- package/templates/angular/src/app/app.config.ts +8 -0
- package/templates/angular/src/app/app.html +19 -0
- package/templates/angular/src/app/app.ts +29 -0
- package/templates/angular/src/app/data.ts +81 -0
- package/templates/angular/src/app/docs.ts +29 -0
- package/templates/angular/src/app/landing.html +77 -0
- package/templates/angular/src/app/landing.ts +29 -0
- package/templates/angular/src/app/overview.html +49 -0
- package/templates/angular/src/app/overview.ts +25 -0
- package/templates/angular/src/app/pathname.ts +24 -0
- package/templates/angular/src/app/signup-form.html +90 -0
- package/templates/angular/src/app/signup-form.ts +65 -0
- package/templates/angular/src/app/site-footer.html +10 -0
- package/templates/angular/src/app/site-footer.ts +22 -0
- package/templates/angular/src/app/site-header.html +30 -0
- package/templates/angular/src/app/site-header.ts +57 -0
- package/templates/angular/src/app.css +355 -0
- package/templates/angular/src/diagnostics.ts +24 -0
- package/templates/angular/src/index.html +12 -0
- package/templates/angular/src/main.ts +21 -0
- package/templates/angular/tsconfig.app.json +9 -0
- package/templates/angular/tsconfig.json +34 -0
- package/templates/react-nextjs/app/app.css +144 -152
- package/templates/react-nextjs/app/data.ts +17 -17
- package/templates/react-nextjs/app/diagnostics.ts +19 -0
- package/templates/react-nextjs/app/docs.ts +9 -9
- package/templates/react-nextjs/app/landing.tsx +63 -121
- package/templates/react-nextjs/app/overview.tsx +25 -27
- package/templates/react-nextjs/app/signup-form.tsx +36 -39
- package/templates/react-nextjs/app/site-footer.tsx +7 -10
- package/templates/react-nextjs/app/site-header.tsx +17 -21
- package/templates/react-nextjs/helix-jsx.d.ts +5 -7
- package/templates/react-nextjs/instrumentation-client.ts +6 -0
- package/templates/react-nextjs/package.json +1 -0
- package/templates/react-nextjs/tsconfig.json +1 -0
- package/templates/react-vite/helix-jsx.d.ts +5 -7
- package/templates/react-vite/package.json +1 -0
- package/templates/react-vite/src/app.css +144 -152
- package/templates/react-vite/src/data.ts +17 -17
- package/templates/react-vite/src/diagnostics.ts +24 -0
- package/templates/react-vite/src/docs.ts +9 -9
- package/templates/react-vite/src/landing.tsx +63 -121
- package/templates/react-vite/src/main.tsx +6 -0
- package/templates/react-vite/src/overview.tsx +24 -26
- package/templates/react-vite/src/routes.tsx +11 -13
- package/templates/react-vite/src/signup-form.tsx +36 -39
- package/templates/react-vite/src/site-footer.tsx +7 -10
- package/templates/react-vite/src/site-header.tsx +15 -19
- package/templates/react-vite/tsconfig.json +1 -0
- package/templates/solid-vite/helix-solid.d.ts +14 -0
- package/templates/solid-vite/index.html +12 -0
- package/templates/solid-vite/package.json +22 -0
- package/templates/solid-vite/src/App.tsx +35 -0
- package/templates/solid-vite/src/app.css +355 -0
- package/templates/solid-vite/src/data.ts +81 -0
- package/templates/solid-vite/src/diagnostics.ts +24 -0
- package/templates/solid-vite/src/docs.ts +29 -0
- package/templates/solid-vite/src/landing.tsx +130 -0
- package/templates/solid-vite/src/main.tsx +23 -0
- package/templates/solid-vite/src/overview.tsx +78 -0
- package/templates/solid-vite/src/routes.ts +22 -0
- package/templates/solid-vite/src/signup-form.tsx +151 -0
- package/templates/solid-vite/src/site-footer.tsx +34 -0
- package/templates/solid-vite/src/site-header.tsx +76 -0
- package/templates/solid-vite/tsconfig.json +27 -0
- package/templates/solid-vite/vite.config.ts +7 -0
- package/templates/svelte-vite/helix-svelte.d.ts +9 -0
- package/templates/svelte-vite/index.html +12 -0
- package/templates/svelte-vite/package.json +23 -0
- package/templates/svelte-vite/src/App.svelte +35 -0
- package/templates/svelte-vite/src/Landing.svelte +100 -0
- package/templates/svelte-vite/src/Overview.svelte +67 -0
- package/templates/svelte-vite/src/SignupForm.svelte +147 -0
- package/templates/svelte-vite/src/SiteFooter.svelte +26 -0
- package/templates/svelte-vite/src/SiteHeader.svelte +71 -0
- package/templates/svelte-vite/src/app.css +355 -0
- package/templates/svelte-vite/src/data.ts +81 -0
- package/templates/svelte-vite/src/diagnostics.ts +24 -0
- package/templates/svelte-vite/src/docs.ts +29 -0
- package/templates/svelte-vite/src/main.ts +23 -0
- package/templates/svelte-vite/src/routes.svelte.ts +19 -0
- package/templates/svelte-vite/tsconfig.json +28 -0
- package/templates/svelte-vite/vite.config.ts +7 -0
- package/templates/vue-vite/helix-vue.d.ts +8 -0
- package/templates/vue-vite/index.html +12 -0
- package/templates/vue-vite/package.json +23 -0
- package/templates/vue-vite/src/App.vue +39 -0
- package/templates/vue-vite/src/LandingPage.vue +102 -0
- package/templates/vue-vite/src/OverviewPage.vue +69 -0
- package/templates/vue-vite/src/SignupForm.vue +149 -0
- package/templates/vue-vite/src/SiteFooter.vue +28 -0
- package/templates/vue-vite/src/SiteHeader.vue +71 -0
- package/templates/vue-vite/src/app.css +355 -0
- package/templates/vue-vite/src/data.ts +81 -0
- package/templates/vue-vite/src/diagnostics.ts +24 -0
- package/templates/vue-vite/src/docs.ts +29 -0
- package/templates/vue-vite/src/main.ts +23 -0
- package/templates/vue-vite/src/routes.ts +26 -0
- package/templates/vue-vite/tsconfig.json +26 -0
- package/templates/vue-vite/vite.config.ts +13 -0
- package/templates.json +41 -0
|
@@ -3,43 +3,43 @@ import { DOCS } from './docs.js'
|
|
|
3
3
|
import { Link } from './routes.js'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* **A locale named
|
|
7
|
-
* argument reads the
|
|
8
|
-
* and the
|
|
9
|
-
* markup React hydrates does not match the markup it
|
|
6
|
+
* **A locale named, so the number reads the same everywhere.**
|
|
7
|
+
* `toLocaleString()` with no argument reads the machine's locale: the server's
|
|
8
|
+
* in the Next.js template, and the browser's in both. The same number is then
|
|
9
|
+
* grouped two ways, and the markup React hydrates does not match the markup it
|
|
10
|
+
* rendered.
|
|
10
11
|
*/
|
|
11
12
|
const figure = new Intl.NumberFormat('en-US')
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
|
-
* The first screen, and the only page here
|
|
15
|
-
* than built out of it.
|
|
15
|
+
* The first screen, and the only page here about the starter itself.
|
|
16
16
|
*
|
|
17
17
|
* Delete it once you know your way around: nothing else imports it, and the
|
|
18
18
|
* route it answers becomes yours.
|
|
19
19
|
*
|
|
20
|
-
* **The
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* **Anchors, not a `Button` with an `href`.** A button that navigates is a link
|
|
26
|
-
* wearing a button's clothes, and the two are different things to a keyboard and
|
|
27
|
-
* to a screen reader. The base stylesheet styles an anchor that asks with
|
|
28
|
-
* `data-button`, so a link can look like one without being one.
|
|
20
|
+
* **The calls to action are anchors.** A button that navigates is a link
|
|
21
|
+
* dressed as a button, and the two are different things to a keyboard and to
|
|
22
|
+
* a screen reader. The base stylesheet styles an anchor that asks with
|
|
23
|
+
* `data-button`, so a link can look like one and stay a link.
|
|
29
24
|
*/
|
|
30
25
|
export function Landing() {
|
|
31
26
|
return (
|
|
32
27
|
<div className="stack">
|
|
33
28
|
<header className="first-screen">
|
|
29
|
+
{/* **The light behind the first screen**: a helix, drawn by the library
|
|
30
|
+
in the theme's colors, so a new brand color relights it. It shows
|
|
31
|
+
one still frame where motion is reduced, and a gradient where the
|
|
32
|
+
browser draws without a GPU. */}
|
|
33
|
+
<helix-aura className="first-light" data-layer speed={0.4} terminus={0.8} variant="braid" />
|
|
34
34
|
<div className="hero">
|
|
35
|
-
<helix-
|
|
36
|
-
|
|
35
|
+
<helix-tag emphasis="subtle" tone="primary">
|
|
36
|
+
Starter
|
|
37
|
+
</helix-tag>
|
|
38
|
+
<helix-text as="h1" variant="display">
|
|
39
|
+
Helix design system
|
|
37
40
|
</helix-text>
|
|
38
|
-
<helix-text as="
|
|
39
|
-
|
|
40
|
-
Nothing here is a wrapper: the tags are the library's own elements, and every color,
|
|
41
|
-
size and space inside them comes from one set of tokens. Three routes are laid down, and
|
|
42
|
-
all of them are yours.
|
|
41
|
+
<helix-text as="p" className="lede" variant="body">
|
|
42
|
+
Web components in your brand's colors, drawn from one set of tokens.
|
|
43
43
|
</helix-text>
|
|
44
44
|
<div className="actions">
|
|
45
45
|
<Link data-button="solid" data-button-tone="primary" href="/overview">
|
|
@@ -51,13 +51,10 @@ export function Landing() {
|
|
|
51
51
|
</div>
|
|
52
52
|
</div>
|
|
53
53
|
|
|
54
|
-
{/* **
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
at a size the type scale does not reach, which is the one place on a
|
|
59
|
-
page where the number is the design. */}
|
|
60
|
-
<helix-card className="specimen" separation="outlined">
|
|
54
|
+
{/* **The library at work, on the first screen.** The figures are the
|
|
55
|
+
roster the data view draws, read from the same module, so the panel
|
|
56
|
+
is the app itself, on glass over the light. */}
|
|
57
|
+
<helix-card className="specimen" material="glass" separation="outlined">
|
|
61
58
|
<helix-text as="h2" slot="header" variant="title">
|
|
62
59
|
The roster, in three figures
|
|
63
60
|
</helix-text>
|
|
@@ -85,106 +82,51 @@ export function Landing() {
|
|
|
85
82
|
<helix-text as="h2" variant="subheading">
|
|
86
83
|
What you have
|
|
87
84
|
</helix-text>
|
|
88
|
-
{/*
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
<
|
|
107
|
-
<helix-text as="h3" variant="title">
|
|
108
|
-
One set of tokens
|
|
109
|
-
</helix-text>
|
|
110
|
-
<helix-text as="p" emphasis="muted" variant="body">
|
|
111
|
-
Every color is written once as a light and dark pair, so the control in the bar moves
|
|
112
|
-
the whole page by setting a single attribute on the root. Type, spacing, radii and
|
|
113
|
-
control sizes come from the same place.
|
|
114
|
-
</helix-text>
|
|
115
|
-
<a data-link href={DOCS.tokens}>
|
|
116
|
-
Tokens
|
|
117
|
-
</a>
|
|
118
|
-
</article>
|
|
119
|
-
|
|
120
|
-
<article className="fact">
|
|
121
|
-
<helix-text as="h3" variant="title">
|
|
122
|
-
A short stylesheet
|
|
123
|
-
</helix-text>
|
|
124
|
-
<helix-text as="p" emphasis="muted" variant="body">
|
|
125
|
-
<helix-code>src/app.css</helix-code> holds this page's measure, the rhythm between its
|
|
126
|
-
blocks, and nothing a component already draws. Where a component leaves a decision
|
|
127
|
-
open it names a part: the data view reaches one to paint the row you tick.
|
|
128
|
-
</helix-text>
|
|
129
|
-
<a data-link href={DOCS.parts}>
|
|
130
|
-
Overriding a component
|
|
131
|
-
</a>
|
|
132
|
-
</article>
|
|
133
|
-
</div>
|
|
134
|
-
</section>
|
|
135
|
-
|
|
136
|
-
<section className="section">
|
|
137
|
-
<helix-text as="h2" variant="subheading">
|
|
138
|
-
Where to go next
|
|
139
|
-
</helix-text>
|
|
140
|
-
{/* **The whole card is the link, which is what `data-card` is for.** A
|
|
141
|
-
card with an anchor in its footer gives a reader a target the size of
|
|
142
|
-
three words inside a box the size of a paragraph, and the box is what
|
|
143
|
-
they aim at. The recipe is in the base layer: it draws the ring, moves
|
|
144
|
-
the card's border to the brand on hover, and leaves focus alone. So
|
|
145
|
-
nothing inside may be interactive, a link inside a link being neither
|
|
146
|
-
valid nor reachable. */}
|
|
147
|
-
<helix-grid columns={2} gap="md">
|
|
148
|
-
<Link data-card href="/overview">
|
|
85
|
+
{/* **Each tile is a link, which is what `data-card` is for.** The base
|
|
86
|
+
layer draws the ring, moves the card's border to the brand on hover,
|
|
87
|
+
and leaves focus alone. Nothing inside may be interactive, since a
|
|
88
|
+
link inside a link is neither valid nor reachable. */}
|
|
89
|
+
<div className="features">
|
|
90
|
+
<a className="feature" data-card href={DOCS.elements}>
|
|
91
|
+
<helix-card separation="outlined">
|
|
92
|
+
<span className="feature-mark">
|
|
93
|
+
<helix-icon name="cube" />
|
|
94
|
+
</span>
|
|
95
|
+
<helix-text as="h3" variant="title">
|
|
96
|
+
Custom elements
|
|
97
|
+
</helix-text>
|
|
98
|
+
<helix-text as="p" emphasis="muted" variant="body">
|
|
99
|
+
One import in <helix-code>src/main.tsx</helix-code> registers every tag.
|
|
100
|
+
</helix-text>
|
|
101
|
+
</helix-card>
|
|
102
|
+
</a>
|
|
103
|
+
<a className="feature" data-card href={DOCS.tokens}>
|
|
149
104
|
<helix-card separation="outlined">
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
The data view
|
|
157
|
-
</helix-text>
|
|
158
|
-
<helix-text emphasis="subtle" variant="code">
|
|
159
|
-
/overview
|
|
160
|
-
</helix-text>
|
|
161
|
-
</helix-flex>
|
|
105
|
+
<span className="feature-mark">
|
|
106
|
+
<helix-icon name="palette" />
|
|
107
|
+
</span>
|
|
108
|
+
<helix-text as="h3" variant="title">
|
|
109
|
+
One set of tokens
|
|
110
|
+
</helix-text>
|
|
162
111
|
<helix-text as="p" emphasis="muted" variant="body">
|
|
163
|
-
|
|
164
|
-
The columns are markup you can read and the rows are one property, which is what
|
|
165
|
-
keeps the same component working at ten thousand.
|
|
112
|
+
Every color, size, and space, in light and dark.
|
|
166
113
|
</helix-text>
|
|
167
114
|
</helix-card>
|
|
168
|
-
</
|
|
169
|
-
|
|
170
|
-
<Link data-card href="/join">
|
|
115
|
+
</a>
|
|
116
|
+
<a className="feature" data-card href={DOCS.parts}>
|
|
171
117
|
<helix-card separation="outlined">
|
|
172
|
-
<
|
|
173
|
-
<helix-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
</helix-text>
|
|
179
|
-
</helix-flex>
|
|
118
|
+
<span className="feature-mark">
|
|
119
|
+
<helix-icon name="sliders-horizontal" />
|
|
120
|
+
</span>
|
|
121
|
+
<helix-text as="h3" variant="title">
|
|
122
|
+
Parts to style
|
|
123
|
+
</helix-text>
|
|
180
124
|
<helix-text as="p" emphasis="muted" variant="body">
|
|
181
|
-
|
|
182
|
-
<helix-code>FormData</helix-code>, because every one of them is a real form
|
|
183
|
-
participant. The email field's complaint is the browser's own.
|
|
125
|
+
Reach inside a component with <helix-code>::part()</helix-code>.
|
|
184
126
|
</helix-text>
|
|
185
127
|
</helix-card>
|
|
186
|
-
</
|
|
187
|
-
</
|
|
128
|
+
</a>
|
|
129
|
+
</div>
|
|
188
130
|
</section>
|
|
189
131
|
</div>
|
|
190
132
|
)
|
|
@@ -15,6 +15,12 @@ import './app.css'
|
|
|
15
15
|
|
|
16
16
|
import { App } from './App.js'
|
|
17
17
|
|
|
18
|
+
/* **Diagnostics in development, and none of it in a build.** `vite build`
|
|
19
|
+
replaces `import.meta.env.DEV` with `false` and drops the import with it. */
|
|
20
|
+
if (import.meta.env.DEV) {
|
|
21
|
+
void import('./diagnostics.js')
|
|
22
|
+
}
|
|
23
|
+
|
|
18
24
|
createRoot(document.querySelector('#root') as HTMLElement).render(
|
|
19
25
|
<StrictMode>
|
|
20
26
|
<App />
|
|
@@ -6,13 +6,13 @@ import { DOCS } from './docs.js'
|
|
|
6
6
|
*
|
|
7
7
|
* **The schema is markup and the data is a property.** The columns are elements
|
|
8
8
|
* you can read in the HTML, and `rows` is the whole result handed over at once,
|
|
9
|
-
*
|
|
10
|
-
* table sorts, pages and selects out of that array on its own
|
|
11
|
-
*
|
|
9
|
+
* since a table of ten thousand rows cannot be written out as elements. The
|
|
10
|
+
* table sorts, pages, and selects out of that array on its own, with no handler
|
|
11
|
+
* for you to write.
|
|
12
12
|
*
|
|
13
13
|
* **Selection is keyed on `row-key`, so it survives a sort and a page.** A row
|
|
14
|
-
* is chosen by its identity
|
|
15
|
-
*
|
|
14
|
+
* is chosen by its identity, wherever it sits, and `app.css` paints the chosen
|
|
15
|
+
* row through the parts the table names for it.
|
|
16
16
|
*/
|
|
17
17
|
export function Overview() {
|
|
18
18
|
return (
|
|
@@ -39,18 +39,18 @@ export function Overview() {
|
|
|
39
39
|
rows={members}
|
|
40
40
|
selectable
|
|
41
41
|
>
|
|
42
|
-
{/* **Every column declares what it may give up,
|
|
42
|
+
{/* **Every column declares what it may give up, which is the whole of
|
|
43
43
|
the narrow-window story.** A column left to itself takes
|
|
44
|
-
`minmax(max-content, 1fr)`, which
|
|
45
|
-
cell
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
344, so at a 376 pixel window they exactly fit and below it the
|
|
52
|
-
scrolls its own box
|
|
53
|
-
|
|
44
|
+
`minmax(max-content, 1fr)`, which never goes below its longest
|
|
45
|
+
cell, so the last column is pushed outside the box and you have to
|
|
46
|
+
scroll the table sideways to read a value. Each column here has a
|
|
47
|
+
floor and a share: the floor is what the words need, and the share
|
|
48
|
+
is how the spare width is divided. Measured September 16, 2026: at
|
|
49
|
+
390 the four columns and the checkboxes come to 358 in a box of
|
|
50
|
+
358, so nothing is cut and nothing wraps. The floors alone come to
|
|
51
|
+
344, so at a 376 pixel window they exactly fit, and below it the
|
|
52
|
+
table scrolls its own box and fades whichever edge has more behind
|
|
53
|
+
it. */}
|
|
54
54
|
<helix-table-column field="name" sortable width="minmax(5.5rem, 1.6fr)">
|
|
55
55
|
Name
|
|
56
56
|
</helix-table-column>
|
|
@@ -60,26 +60,24 @@ export function Overview() {
|
|
|
60
60
|
<helix-table-column field="role" width="minmax(4.5rem, 1fr)">
|
|
61
61
|
Role
|
|
62
62
|
</helix-table-column>
|
|
63
|
-
{/* `end` lines the digits up under each other, which is
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
take a share of anything. */}
|
|
63
|
+
{/* `end` lines the digits up under each other, which is what makes a
|
|
64
|
+
number column different from a text one. `max-content`, since a
|
|
65
|
+
column of numerals has one right width. */}
|
|
67
66
|
<helix-table-column field="reviews" sortable text-align="end" width="max-content">
|
|
68
67
|
Reviews
|
|
69
68
|
</helix-table-column>
|
|
70
69
|
|
|
71
|
-
{/* The footer is a row across the table
|
|
72
|
-
|
|
70
|
+
{/* The footer is a row across the table: anything you slot goes at the
|
|
71
|
+
start, and the table's own pagination sits at the end. */}
|
|
73
72
|
<helix-text emphasis="subtle" slot="footer" variant="caption">
|
|
74
73
|
{members.length} people
|
|
75
74
|
</helix-text>
|
|
76
75
|
</helix-data-table>
|
|
77
76
|
|
|
78
|
-
{/* A footnote to the table
|
|
79
|
-
takes the section's gap and not the stack's. */}
|
|
77
|
+
{/* A footnote to the table, so it takes the section's gap. */}
|
|
80
78
|
<helix-text as="p" emphasis="muted" variant="caption">
|
|
81
|
-
Filtering, column reordering, resizing, virtualized rows and a server mode
|
|
82
|
-
|
|
79
|
+
Filtering, column reordering, resizing, virtualized rows, and a server mode for large
|
|
80
|
+
results are on{' '}
|
|
83
81
|
<a data-link href={DOCS.table}>
|
|
84
82
|
the component's own page
|
|
85
83
|
</a>
|
|
@@ -2,26 +2,24 @@ import { type AnchorHTMLAttributes, useEffect, useState } from 'react'
|
|
|
2
2
|
|
|
3
3
|
/* **The whole router, which is a hash and a listener.**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
Three pages need `location.hash` and one event, so the starter leaves the
|
|
6
|
+
router to you. Swap this file for the router you want: the two names it
|
|
7
|
+
exports are Next.js's, so the rest of the app reads the same in either
|
|
8
|
+
template, and nothing else has to change.
|
|
9
9
|
|
|
10
|
-
A hash
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
either. */
|
|
10
|
+
A hash, because the History API wants a server that answers every path with
|
|
11
|
+
the same document, and `vite preview` and wherever you deploy each have to be
|
|
12
|
+
told to. A hash asks nothing of either. */
|
|
14
13
|
|
|
15
|
-
/**
|
|
14
|
+
/** The current path, read from the hash, so the first page is `/`. */
|
|
16
15
|
const read = (): string => window.location.hash.slice(1) || '/'
|
|
17
16
|
|
|
18
17
|
/**
|
|
19
18
|
* A destination, addressed the way the Next.js template addresses one.
|
|
20
19
|
*
|
|
21
|
-
* Everything but `href` goes straight through to the anchor,
|
|
22
|
-
*
|
|
23
|
-
* templates. The path is written as a path
|
|
24
|
-
* business and nobody else's.
|
|
20
|
+
* Everything but `href` goes straight through to the anchor, as `next/link`
|
|
21
|
+
* does, which is why a `data-button` or a `slot` reads the same in both
|
|
22
|
+
* templates. The path is written as a path, and this component adds the hash.
|
|
25
23
|
*/
|
|
26
24
|
export function Link({ href, ...rest }: AnchorHTMLAttributes<HTMLAnchorElement>) {
|
|
27
25
|
return <a {...rest} href={`#${href ?? '/'}`} />
|
|
@@ -12,21 +12,21 @@ interface Message {
|
|
|
12
12
|
* The form.
|
|
13
13
|
*
|
|
14
14
|
* **Every control here is a form-associated custom element**, so a `<form>`
|
|
15
|
-
* collects them, resets them and validates them the way it does an `<input>`.
|
|
16
|
-
*
|
|
17
|
-
* the answers with `new FormData(form)
|
|
15
|
+
* collects them, resets them, and validates them the way it does an `<input>`.
|
|
16
|
+
* `ElementInternals` makes that possible, and the submit below proves it: it
|
|
17
|
+
* reads the answers with `new FormData(form)`, and nothing gathers them by hand.
|
|
18
18
|
*
|
|
19
|
-
* There is no wrapping `Field` component. A label in one shadow root cannot
|
|
20
|
-
* a control in another, so every control renders its own `label`, `hint
|
|
21
|
-
* `feedback
|
|
19
|
+
* There is no wrapping `Field` component. A label in one shadow root cannot
|
|
20
|
+
* name a control in another, so every control renders its own `label`, `hint`,
|
|
21
|
+
* and `feedback`.
|
|
22
22
|
*/
|
|
23
23
|
export function SignupForm() {
|
|
24
24
|
const [handle, setHandle] = useState('')
|
|
25
25
|
const [messages, setMessages] = useState<readonly Message[]>([])
|
|
26
26
|
|
|
27
27
|
const join = (event: FormEvent<HTMLFormElement>) => {
|
|
28
|
-
/* The page
|
|
29
|
-
posts to the server like any other, with the same names. */
|
|
28
|
+
/* The page takes the submit, so nothing navigates. Remove the line and the
|
|
29
|
+
form posts to the server like any other, with the same names. */
|
|
30
30
|
event.preventDefault()
|
|
31
31
|
const answers = [...new FormData(event.currentTarget).entries()]
|
|
32
32
|
.map(([field, value]) => `${field}=${value}`)
|
|
@@ -49,18 +49,16 @@ export function SignupForm() {
|
|
|
49
49
|
</helix-text>
|
|
50
50
|
</header>
|
|
51
51
|
|
|
52
|
-
{/* The form takes a column of its own and the notes take what is left
|
|
53
|
-
|
|
54
|
-
the two stack and the notes read as a postscript. */}
|
|
52
|
+
{/* The form takes a column of its own and the notes take what is left. At
|
|
53
|
+
390 the two stack, and the notes read as a postscript. */}
|
|
55
54
|
<div className="split">
|
|
56
55
|
<form className="form" onSubmit={join}>
|
|
57
56
|
<helix-input autocomplete="name" label="Name" name="name" required />
|
|
58
57
|
|
|
59
58
|
{/* Validation is the platform's: `type` and `required` are set on the
|
|
60
|
-
real input inside, and its own message
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
mistake. */}
|
|
59
|
+
real input inside, and its own message shows once the field has been
|
|
60
|
+
touched. Set `feedback` to say it in your own words, and it shows
|
|
61
|
+
from the first paint. */}
|
|
64
62
|
<helix-input
|
|
65
63
|
hint="Where we send the welcome note."
|
|
66
64
|
label="Email"
|
|
@@ -72,11 +70,11 @@ export function SignupForm() {
|
|
|
72
70
|
{/* **A controlled value, which is the one habit to unlearn.** The handler
|
|
73
71
|
is a prop named for the event, so `onhelix-before-change` hears
|
|
74
72
|
`helix-before-change`. A Helix element is not read-only: writing
|
|
75
|
-
`value` seeds it and the element goes on applying its own changes
|
|
76
|
-
taking the decision means refusing the change with
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
`value` seeds it, and the element goes on applying its own changes.
|
|
74
|
+
So taking the decision means refusing the change with
|
|
75
|
+
`preventDefault()`, then setting the value you want. Leaving
|
|
76
|
+
`setHandle` uncalled refuses nothing, and the element keeps the value
|
|
77
|
+
you meant to turn down. */}
|
|
80
78
|
<helix-input
|
|
81
79
|
hint="Lowercase and hyphens, whatever you type."
|
|
82
80
|
label="Handle"
|
|
@@ -96,8 +94,8 @@ export function SignupForm() {
|
|
|
96
94
|
))}
|
|
97
95
|
</helix-select>
|
|
98
96
|
|
|
99
|
-
{/* A switch is a thing happening now
|
|
100
|
-
|
|
97
|
+
{/* A switch is a thing happening now, which is why it says on and off.
|
|
98
|
+
It still submits, as `on`. */}
|
|
101
99
|
<helix-switch label="Send me the weekly digest" name="digest" />
|
|
102
100
|
|
|
103
101
|
<helix-button emphasis="solid" tone="primary" type="submit">
|
|
@@ -106,27 +104,26 @@ export function SignupForm() {
|
|
|
106
104
|
</form>
|
|
107
105
|
|
|
108
106
|
<aside className="notes">
|
|
109
|
-
{/* The one fact
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
read in grayscale. */}
|
|
107
|
+
{/* The one fact you need before typing your name into a page you have
|
|
108
|
+
had for five minutes, so it is a callout. `tone` is the meaning,
|
|
109
|
+
and the component draws it with a tint mixed from the status color
|
|
110
|
+
and a bar on the inline start, so it survives being read in
|
|
111
|
+
grayscale. */}
|
|
115
112
|
<helix-callout tone="info">
|
|
116
|
-
|
|
117
|
-
|
|
113
|
+
The form posts nothing. Submit it, and a message shows exactly what a server would
|
|
114
|
+
receive.
|
|
118
115
|
</helix-callout>
|
|
119
116
|
<helix-text as="h2" variant="title">
|
|
120
|
-
What
|
|
117
|
+
What the form shows
|
|
121
118
|
</helix-text>
|
|
122
119
|
<helix-text as="p" emphasis="muted" variant="body">
|
|
123
|
-
Every control is a form-associated custom element, so the submit reads them
|
|
124
|
-
<helix-code>new FormData(form)</helix-code
|
|
125
|
-
|
|
120
|
+
Every control is a form-associated custom element, so the submit reads them all
|
|
121
|
+
with <helix-code>new FormData(form)</helix-code>. Reset the form and they all go back,
|
|
122
|
+
the way an <helix-code>input</helix-code> does.
|
|
126
123
|
</helix-text>
|
|
127
124
|
<helix-text as="p" emphasis="muted" variant="body">
|
|
128
|
-
Leave the email wrong and the
|
|
129
|
-
|
|
125
|
+
Leave the email wrong and the browser shows its own message, in your language. Handle is
|
|
126
|
+
the controlled field: the page refuses each change and writes back a cleaned value.
|
|
130
127
|
</helix-text>
|
|
131
128
|
<a data-link href={DOCS.forms}>
|
|
132
129
|
Forms
|
|
@@ -136,9 +133,9 @@ export function SignupForm() {
|
|
|
136
133
|
|
|
137
134
|
{/* **The region is rendered whether or not there is anything in it.**
|
|
138
135
|
Assistive technology subscribes to a live region, and one that arrives
|
|
139
|
-
already holding its message is
|
|
140
|
-
creates or destroys
|
|
141
|
-
|
|
136
|
+
already holding its message is often never announced. Messages are
|
|
137
|
+
state, so nothing creates or destroys one here, and a dismissed one
|
|
138
|
+
asks to go through `helix-dismiss`. */}
|
|
142
139
|
<helix-toast>
|
|
143
140
|
{messages.map((message) => (
|
|
144
141
|
<helix-toast-item
|
|
@@ -3,23 +3,20 @@ import { DOCS } from './docs.js'
|
|
|
3
3
|
/**
|
|
4
4
|
* The bar's counterpart, on every route.
|
|
5
5
|
*
|
|
6
|
-
* **It
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* a reader wants once rather than constantly belong, which is why the landing
|
|
11
|
-
* page no longer carries them itself.
|
|
6
|
+
* **It puts a floor under the content.** The overview is a heading, a table,
|
|
7
|
+
* and a caption, which on a tall window left the bottom third of the screen
|
|
8
|
+
* empty, as if the layout had run out. The footer is also where the links you
|
|
9
|
+
* want once belong, so the landing page no longer carries them.
|
|
12
10
|
*
|
|
13
|
-
* `data-link="subtle"` reads as body text until it is hovered, which
|
|
14
|
-
* row of links under a rule
|
|
15
|
-
* asking to be read.
|
|
11
|
+
* `data-link="subtle"` reads as body text until it is hovered, which suits a
|
|
12
|
+
* row of links under a rule.
|
|
16
13
|
*/
|
|
17
14
|
export function SiteFooter() {
|
|
18
15
|
return (
|
|
19
16
|
<footer className="site-footer">
|
|
20
17
|
<div className="footer-row">
|
|
21
18
|
<helix-text as="p" emphasis="subtle" variant="caption">
|
|
22
|
-
Every component
|
|
19
|
+
Every component here has a page, with its API, its parts, and its examples.
|
|
23
20
|
</helix-text>
|
|
24
21
|
<div className="footer-links">
|
|
25
22
|
<a data-link="subtle" href={DOCS.gallery}>
|
|
@@ -18,18 +18,16 @@ export function SiteHeader() {
|
|
|
18
18
|
control draws the same glyph on the server and on the first client paint. */
|
|
19
19
|
const [mode, setMode] = useState<Mode>('system')
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/**
|
|
22
22
|
* One attribute, two modes, and no second stylesheet.
|
|
23
23
|
*
|
|
24
|
-
* Every Helix token is written once as a `light-dark()` pair, so
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* a dark page.
|
|
24
|
+
* Every Helix token is written once as a `light-dark()` pair, so
|
|
25
|
+
* `color-scheme` decides which half resolves. `data-color-mode` sets it, and
|
|
26
|
+
* since `color-scheme` inherits, the attribute works on any element: put it
|
|
27
|
+
* on a panel for a light preview inside a dark page.
|
|
29
28
|
*
|
|
30
|
-
* `system` removes the attribute
|
|
31
|
-
*
|
|
32
|
-
* changed.
|
|
29
|
+
* `system` removes the attribute. Writing out whichever mode is current would
|
|
30
|
+
* hold the page there when the system's own setting changed.
|
|
33
31
|
*/
|
|
34
32
|
const cycle = () => {
|
|
35
33
|
const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length]
|
|
@@ -43,11 +41,10 @@ export function SiteHeader() {
|
|
|
43
41
|
|
|
44
42
|
return (
|
|
45
43
|
<helix-app-header material="glass" sticky>
|
|
46
|
-
{/* The wordmark is the way back to the first page,
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
it.
|
|
44
|
+
{/* The wordmark is the way back to the first page, as a wordmark usually
|
|
45
|
+
is, so there is no third destination in the bar saying the same thing.
|
|
46
|
+
It is also the only name of the project on the screen: the landing
|
|
47
|
+
page leads with what the starter is.
|
|
51
48
|
|
|
52
49
|
The mark is a placeholder for your own, and it is drawn in
|
|
53
50
|
`--helix-color-brand-mark`, the token for the brand as a graphic. */}
|
|
@@ -57,8 +54,8 @@ export function SiteHeader() {
|
|
|
57
54
|
</Link>
|
|
58
55
|
|
|
59
56
|
{/* The links are yours. Navigation owns the marker that travels between
|
|
60
|
-
them and renders no anchors of its own,
|
|
61
|
-
own
|
|
57
|
+
them and renders no anchors of its own, since a router renders its
|
|
58
|
+
own, which could not sit inside this shadow root. The base layer
|
|
62
59
|
styles them, `aria-current="page"` included. */}
|
|
63
60
|
<helix-navigation label="Pages">
|
|
64
61
|
<Link aria-current={path === '/overview' ? 'page' : undefined} href="/overview">
|
|
@@ -69,9 +66,8 @@ export function SiteHeader() {
|
|
|
69
66
|
</Link>
|
|
70
67
|
</helix-navigation>
|
|
71
68
|
|
|
72
|
-
{/* An icon with no words has no accessible name
|
|
73
|
-
which is
|
|
74
|
-
with a glyph in it. */}
|
|
69
|
+
{/* An icon with no words has no accessible name until it is given one,
|
|
70
|
+
which is why this is its own element. */}
|
|
75
71
|
<helix-icon-button
|
|
76
72
|
label={`Color mode: ${mode}`}
|
|
77
73
|
onClick={cycle}
|