@fusion.dev/helix-templates 0.138.0 → 0.139.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/README.md +18 -6
  2. package/dist/index.js +24 -0
  3. package/package.json +1 -1
  4. package/templates/angular/angular.json +57 -0
  5. package/templates/angular/package.json +28 -0
  6. package/templates/angular/src/app/app.config.ts +8 -0
  7. package/templates/angular/src/app/app.html +19 -0
  8. package/templates/angular/src/app/app.ts +29 -0
  9. package/templates/angular/src/app/data.ts +81 -0
  10. package/templates/angular/src/app/docs.ts +29 -0
  11. package/templates/angular/src/app/landing.html +112 -0
  12. package/templates/angular/src/app/landing.ts +31 -0
  13. package/templates/angular/src/app/overview.html +49 -0
  14. package/templates/angular/src/app/overview.ts +25 -0
  15. package/templates/angular/src/app/pathname.ts +24 -0
  16. package/templates/angular/src/app/signup-form.html +91 -0
  17. package/templates/angular/src/app/signup-form.ts +65 -0
  18. package/templates/angular/src/app/site-footer.html +10 -0
  19. package/templates/angular/src/app/site-footer.ts +22 -0
  20. package/templates/angular/src/app/site-header.html +30 -0
  21. package/templates/angular/src/app/site-header.ts +58 -0
  22. package/templates/angular/src/app.css +363 -0
  23. package/templates/angular/src/diagnostics.ts +24 -0
  24. package/templates/angular/src/index.html +12 -0
  25. package/templates/angular/src/main.ts +21 -0
  26. package/templates/angular/tsconfig.app.json +9 -0
  27. package/templates/angular/tsconfig.json +34 -0
  28. package/templates/react-nextjs/app/diagnostics.ts +19 -0
  29. package/templates/react-nextjs/app/landing.tsx +2 -2
  30. package/templates/react-nextjs/app/overview.tsx +1 -1
  31. package/templates/react-nextjs/instrumentation-client.ts +6 -0
  32. package/templates/react-nextjs/package.json +1 -0
  33. package/templates/react-nextjs/tsconfig.json +1 -0
  34. package/templates/react-vite/package.json +1 -0
  35. package/templates/react-vite/src/diagnostics.ts +24 -0
  36. package/templates/react-vite/src/main.tsx +6 -0
  37. package/templates/react-vite/tsconfig.json +1 -0
  38. package/templates/solid-vite/helix-solid.d.ts +14 -0
  39. package/templates/solid-vite/index.html +12 -0
  40. package/templates/solid-vite/package.json +22 -0
  41. package/templates/solid-vite/src/App.tsx +35 -0
  42. package/templates/solid-vite/src/app.css +363 -0
  43. package/templates/solid-vite/src/data.ts +81 -0
  44. package/templates/solid-vite/src/diagnostics.ts +24 -0
  45. package/templates/solid-vite/src/docs.ts +29 -0
  46. package/templates/solid-vite/src/landing.tsx +177 -0
  47. package/templates/solid-vite/src/main.tsx +23 -0
  48. package/templates/solid-vite/src/overview.tsx +78 -0
  49. package/templates/solid-vite/src/routes.ts +22 -0
  50. package/templates/solid-vite/src/signup-form.tsx +151 -0
  51. package/templates/solid-vite/src/site-footer.tsx +34 -0
  52. package/templates/solid-vite/src/site-header.tsx +77 -0
  53. package/templates/solid-vite/tsconfig.json +27 -0
  54. package/templates/solid-vite/vite.config.ts +7 -0
  55. package/templates/svelte-vite/helix-svelte.d.ts +9 -0
  56. package/templates/svelte-vite/index.html +12 -0
  57. package/templates/svelte-vite/package.json +23 -0
  58. package/templates/svelte-vite/src/App.svelte +35 -0
  59. package/templates/svelte-vite/src/Landing.svelte +137 -0
  60. package/templates/svelte-vite/src/Overview.svelte +67 -0
  61. package/templates/svelte-vite/src/SignupForm.svelte +148 -0
  62. package/templates/svelte-vite/src/SiteFooter.svelte +26 -0
  63. package/templates/svelte-vite/src/SiteHeader.svelte +72 -0
  64. package/templates/svelte-vite/src/app.css +363 -0
  65. package/templates/svelte-vite/src/data.ts +81 -0
  66. package/templates/svelte-vite/src/diagnostics.ts +24 -0
  67. package/templates/svelte-vite/src/docs.ts +29 -0
  68. package/templates/svelte-vite/src/main.ts +23 -0
  69. package/templates/svelte-vite/src/routes.svelte.ts +19 -0
  70. package/templates/svelte-vite/tsconfig.json +28 -0
  71. package/templates/svelte-vite/vite.config.ts +7 -0
  72. package/templates/vue-vite/helix-vue.d.ts +8 -0
  73. package/templates/vue-vite/index.html +12 -0
  74. package/templates/vue-vite/package.json +23 -0
  75. package/templates/vue-vite/src/App.vue +39 -0
  76. package/templates/vue-vite/src/LandingPage.vue +139 -0
  77. package/templates/vue-vite/src/OverviewPage.vue +69 -0
  78. package/templates/vue-vite/src/SignupForm.vue +149 -0
  79. package/templates/vue-vite/src/SiteFooter.vue +28 -0
  80. package/templates/vue-vite/src/SiteHeader.vue +72 -0
  81. package/templates/vue-vite/src/app.css +363 -0
  82. package/templates/vue-vite/src/data.ts +81 -0
  83. package/templates/vue-vite/src/diagnostics.ts +24 -0
  84. package/templates/vue-vite/src/docs.ts +29 -0
  85. package/templates/vue-vite/src/main.ts +23 -0
  86. package/templates/vue-vite/src/routes.ts +26 -0
  87. package/templates/vue-vite/tsconfig.json +26 -0
  88. package/templates/vue-vite/vite.config.ts +13 -0
  89. package/templates.json +41 -0
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The rows the overview draws.
3
+ *
4
+ * A local module and no network, so the starter runs with nothing behind it.
5
+ * Point `rows` at whatever you fetch and the table does not change: it takes the
6
+ * whole result and does the sorting, the filtering and the paging itself. Past
7
+ * the size a browser can hold, `mode="server"` inverts that and the table asks
8
+ * you for a page instead.
9
+ */
10
+
11
+ /**
12
+ * One person on the roster.
13
+ *
14
+ * **A type alias and not an interface**, which TypeScript makes load-bearing
15
+ * here: `rows` takes `Readonly<Record<string, unknown>>`, an interface has no
16
+ * implicit index signature, and so an interface of exactly these fields is not
17
+ * assignable to it. The error names the index signature and not the shape, so
18
+ * it reads as though the data were wrong.
19
+ */
20
+ export type Member = {
21
+ /**
22
+ * A stable identity, which is what `row-key` names.
23
+ *
24
+ * Sorting moves a row's element rather than rewriting every cell in it, so
25
+ * without one a sort throws away focus and anything attached to a row.
26
+ */
27
+ readonly id: string
28
+ /**
29
+ * What the reader is here to find.
30
+ */
31
+ readonly name: string
32
+ /**
33
+ * A number rather than a formatted string, so the column sorts by size.
34
+ *
35
+ * A cell showing `2.4k` sorts as text unless the column's `compare` says what
36
+ * the value underneath is, which is the one thing here that is cheap to get
37
+ * right now and expensive to add later.
38
+ */
39
+ readonly reviews: number
40
+ /**
41
+ * What they do.
42
+ */
43
+ readonly role: string
44
+ /**
45
+ * Who they do it with.
46
+ */
47
+ readonly team: string
48
+ }
49
+
50
+ export const members: readonly Member[] = [
51
+ { id: 'm-01', name: 'Ada Okonkwo', reviews: 184, role: 'Staff', team: 'Platform' },
52
+ { id: 'm-02', name: 'Bruno Salas', reviews: 96, role: 'Senior', team: 'Payments' },
53
+ { id: 'm-03', name: 'Cai Wen', reviews: 231, role: 'Principal', team: 'Platform' },
54
+ { id: 'm-04', name: 'Dilara Yilmaz', reviews: 64, role: 'Senior', team: 'Growth' },
55
+ { id: 'm-05', name: 'Esi Mensah', reviews: 148, role: 'Staff', team: 'Payments' },
56
+ { id: 'm-06', name: 'Felix Braun', reviews: 39, role: 'Engineer', team: 'Growth' },
57
+ { id: 'm-07', name: 'Gita Raman', reviews: 112, role: 'Senior', team: 'Platform' },
58
+ { id: 'm-08', name: 'Hana Sato', reviews: 57, role: 'Engineer', team: 'Design' },
59
+ { id: 'm-09', name: 'Ivan Petrov', reviews: 88, role: 'Senior', team: 'Payments' },
60
+ { id: 'm-10', name: 'Jo Lindqvist', reviews: 21, role: 'Engineer', team: 'Design' },
61
+ { id: 'm-11', name: 'Kwame Asante', reviews: 73, role: 'Staff', team: 'Growth' },
62
+ { id: 'm-12', name: 'Lena Fischer', reviews: 12, role: 'Engineer', team: 'Design' },
63
+ ]
64
+
65
+ /** The teams the form offers, read from the roster so the two cannot disagree. */
66
+ export const teams: readonly string[] = [...new Set(members.map((one) => one.team))].toSorted()
67
+
68
+ /**
69
+ * The same roster as three figures, for the panel on the first screen.
70
+ *
71
+ * Derived rather than written out, so the panel cannot claim a number the table
72
+ * does not show.
73
+ */
74
+ export const roster = {
75
+ /** How many rows the data view has. */
76
+ people: members.length,
77
+ /** Every review on the roster, added up. */
78
+ reviews: members.reduce((total, one) => total + one.reviews, 0),
79
+ /** How many teams they are spread across. */
80
+ teams: teams.length,
81
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Helix diagnostics, which `main.tsx` loads in development only.
3
+ *
4
+ * It finds the Helix mistakes that are valid CSS and therefore silent, such as
5
+ * a `::part()` naming a part nothing publishes, and an attribute value outside
6
+ * the set a component takes. Each finding prints once in DevTools, and a badge
7
+ * in the corner of the page lists them while there are any.
8
+ *
9
+ * **Checked against the Helix you installed**, from the lint data it ships, so
10
+ * nothing is loaded from the network.
11
+ *
12
+ * To print the findings in your terminal as well, run `npx helix diagnostics`
13
+ * from `@fusion.dev/helix-cli` beside the dev server, and add
14
+ * `sinks: { relay: true }` below.
15
+ */
16
+ import { initDiagnostics } from '@fusion.dev/helix-diagnostics'
17
+ import lint from '@fusion.dev/helix/lint.json'
18
+
19
+ initDiagnostics({
20
+ environment: 'development',
21
+ /* The route is in the hash, so each page's findings are kept apart. */
22
+ page: () => window.location.hash.slice(1) || '/',
23
+ tiers: [{ lint }],
24
+ })
@@ -0,0 +1,29 @@
1
+ /**
2
+ * The documentation this starter links to.
3
+ *
4
+ * **One module, because a link is the thing in a starter most likely to rot.**
5
+ * A page that composes its own URLs ships a plausible-looking 404 to every
6
+ * project scaffolded from it, and nobody finds out until a reader clicks one.
7
+ * Here they are in a list somebody can open and check, and pointing the starter
8
+ * at a mirror or an internal copy is one edit.
9
+ *
10
+ * Checked 16 Sep 2026: every entry answers 200.
11
+ */
12
+ export const DOCS = {
13
+ /** How the elements are registered and written, which is the first page to read. */
14
+ elements: 'https://fusion.dev/helix/docs/using-the-elements',
15
+ /** What a `<form>` does with these controls, and what it cannot do. */
16
+ forms: 'https://fusion.dev/helix/docs/forms',
17
+ /** Every component, with its API, its parts and its examples. */
18
+ gallery: 'https://fusion.dev/helix/components',
19
+ /** The light and dark pair, and the attribute that chooses between them. */
20
+ modes: 'https://fusion.dev/helix/docs/color-modes',
21
+ /** Reaching a component's parts from a page, which `app.css` does once. */
22
+ parts: 'https://fusion.dev/helix/docs/overriding-a-component',
23
+ /** Solid and the other frameworks: props, events, and seeded values. */
24
+ solid: 'https://fusion.dev/helix/docs/other-frameworks',
25
+ /** The data view's own page, where the rest of its API is. */
26
+ table: 'https://fusion.dev/helix/components/data-table',
27
+ /** Every token, and what each one is for. */
28
+ tokens: 'https://fusion.dev/helix/docs/tokens',
29
+ } as const
@@ -0,0 +1,177 @@
1
+ import { roster } from './data.ts'
2
+ import { DOCS } from './docs.ts'
3
+
4
+ /**
5
+ * **A locale named, so the number reads the same everywhere.**
6
+ * `toLocaleString()` with no argument reads the machine's locale, so the same
7
+ * number is grouped two ways on two machines.
8
+ */
9
+ const figure = new Intl.NumberFormat('en-US')
10
+
11
+ /**
12
+ * The first screen, and the only page here about the starter itself.
13
+ *
14
+ * Delete it once you know your way around: nothing else imports it, and the
15
+ * route it answers becomes yours.
16
+ *
17
+ * **The heading says what this is, and the project's name stays in the bar.**
18
+ *
19
+ * **The calls to action are anchors.** A button that navigates is a link
20
+ * dressed as a button, and the two are different things to a keyboard and to
21
+ * a screen reader. The base stylesheet styles an anchor that asks with
22
+ * `data-button`, so a link can look like one and stay a link.
23
+ */
24
+ export function Landing() {
25
+ return (
26
+ <div class="stack">
27
+ <header class="first-screen">
28
+ <div class="hero">
29
+ <helix-text emphasis="muted" variant="label">
30
+ Helix starter
31
+ </helix-text>
32
+ <helix-text as="h1">Every tag on this page is Helix</helix-text>
33
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
34
+ Nothing here is a wrapper: the tags are the library's own elements, and every color,
35
+ size and space inside them comes from one set of tokens. Three routes are laid down, and
36
+ all of them are yours.
37
+ </helix-text>
38
+ <div class="actions">
39
+ <a data-button="solid" data-button-tone="primary" href="#/overview">
40
+ See the data view
41
+ </a>
42
+ <a data-button href="#/join">
43
+ Open the form
44
+ </a>
45
+ </div>
46
+ </div>
47
+
48
+ {/* The figures are the roster the data view draws, read from the same
49
+ module, so the panel is the app at work. `helix-stat` sets the
50
+ number on the display face at a size the type scale does not
51
+ reach. */}
52
+ <helix-card class="specimen" separation="outlined">
53
+ <helix-text as="h2" slot="header" variant="title">
54
+ The roster, in three figures
55
+ </helix-text>
56
+ <helix-stat-group divided>
57
+ <helix-stat>
58
+ {roster.people}
59
+ <span slot="label">People</span>
60
+ </helix-stat>
61
+ <helix-stat>
62
+ {roster.teams}
63
+ <span slot="label">Teams</span>
64
+ </helix-stat>
65
+ <helix-stat>
66
+ {figure.format(roster.reviews)}
67
+ <span slot="label">Reviews</span>
68
+ </helix-stat>
69
+ </helix-stat-group>
70
+ <a data-link href="#/overview" slot="footer">
71
+ Open the data view
72
+ </a>
73
+ </helix-card>
74
+ </header>
75
+
76
+ <section class="section">
77
+ <helix-text as="h2" variant="subheading">
78
+ What you have
79
+ </helix-text>
80
+ {/* Three paragraphs about one installed thing, so they take a rule each
81
+ and no box. `app.css` says why at more length. */}
82
+ <div class="facts">
83
+ <article class="fact">
84
+ <helix-text as="h3" variant="title">
85
+ Elements, not wrappers
86
+ </helix-text>
87
+ <helix-text as="p" emphasis="muted" variant="body">
88
+ They are custom elements, so you write the tags and there is no per-component import
89
+ to keep in step. One line in <helix-code>src/main.tsx</helix-code> registers all of
90
+ them, and a subpath like <helix-code>@fusion.dev/helix/tabs</helix-code> registers
91
+ one.
92
+ </helix-text>
93
+ <a data-link href={DOCS.elements}>
94
+ Using the elements
95
+ </a>
96
+ </article>
97
+
98
+ <article class="fact">
99
+ <helix-text as="h3" variant="title">
100
+ One set of tokens
101
+ </helix-text>
102
+ <helix-text as="p" emphasis="muted" variant="body">
103
+ Every color is written once as a light and dark pair, so the control in the bar moves
104
+ the whole page by setting a single attribute on the root. Type, spacing, radii and
105
+ control sizes come from the same place.
106
+ </helix-text>
107
+ <a data-link href={DOCS.tokens}>
108
+ Tokens
109
+ </a>
110
+ </article>
111
+
112
+ <article class="fact">
113
+ <helix-text as="h3" variant="title">
114
+ A short stylesheet
115
+ </helix-text>
116
+ <helix-text as="p" emphasis="muted" variant="body">
117
+ <helix-code>src/app.css</helix-code> holds this page's measure, the rhythm between its
118
+ blocks, and nothing a component already draws. Where a component leaves a decision
119
+ open it names a part: the data view reaches one to paint the row you tick.
120
+ </helix-text>
121
+ <a data-link href={DOCS.parts}>
122
+ Overriding a component
123
+ </a>
124
+ </article>
125
+ </div>
126
+ </section>
127
+
128
+ <section class="section">
129
+ <helix-text as="h2" variant="subheading">
130
+ Where to go next
131
+ </helix-text>
132
+ {/* **The whole card is the link, which is what `data-card` is for.** The
133
+ base layer draws the ring, moves the card's border to the brand on
134
+ hover, and leaves focus alone. Nothing inside may be interactive,
135
+ since a link inside a link is neither valid nor reachable. */}
136
+ <helix-grid columns={2} gap="md">
137
+ <a data-card href="#/overview">
138
+ <helix-card separation="outlined">
139
+ {/* The route beside the name, on one baseline in the header. */}
140
+ <helix-flex align-items="baseline" gap="sm" justify-content="between" slot="header">
141
+ <helix-text as="h3" variant="title">
142
+ The data view
143
+ </helix-text>
144
+ <helix-text emphasis="subtle" variant="code">
145
+ /overview
146
+ </helix-text>
147
+ </helix-flex>
148
+ <helix-text as="p" emphasis="muted" variant="body">
149
+ Twelve people from a local module, sorted, paged and selected with nothing wired up.
150
+ The columns are markup you can read and the rows are one property, which is what
151
+ keeps the same component working at ten thousand.
152
+ </helix-text>
153
+ </helix-card>
154
+ </a>
155
+
156
+ <a data-card href="#/join">
157
+ <helix-card separation="outlined">
158
+ <helix-flex align-items="baseline" gap="sm" justify-content="between" slot="header">
159
+ <helix-text as="h3" variant="title">
160
+ The form
161
+ </helix-text>
162
+ <helix-text emphasis="subtle" variant="code">
163
+ /join
164
+ </helix-text>
165
+ </helix-flex>
166
+ <helix-text as="p" emphasis="muted" variant="body">
167
+ Five controls a <helix-code>&lt;form&gt;</helix-code> reads with{' '}
168
+ <helix-code>FormData</helix-code>, because every one of them is a real form
169
+ participant. The email field's complaint is the browser's own.
170
+ </helix-text>
171
+ </helix-card>
172
+ </a>
173
+ </helix-grid>
174
+ </section>
175
+ </div>
176
+ )
177
+ }
@@ -0,0 +1,23 @@
1
+ import { render } from 'solid-js/web'
2
+
3
+ /* Every layer in one import: the tokens, the reset, the utility layer, and the
4
+ `:not(:defined)` rules that keep a tag from arriving unstyled. It reaches its
5
+ pieces by bare specifier, so a bundler resolves it and a `<link>` cannot. */
6
+ import '@fusion.dev/helix-styles/helix.css'
7
+ /* A custom element registers as a side effect of being imported, so this line
8
+ makes every Helix tag in the app real. Import the whole library while you are
9
+ finding your feet. `@fusion.dev/helix/button` and its siblings are subpath
10
+ exports, for when you want to pay for only what you use. */
11
+ import '@fusion.dev/helix'
12
+ /* After both, so a rule of yours that names the same thing wins. */
13
+ import './app.css'
14
+
15
+ import { App } from './App.tsx'
16
+
17
+ /* **Diagnostics in development, and none of it in a build.** `vite build`
18
+ replaces `import.meta.env.DEV` with `false` and drops the import with it. */
19
+ if (import.meta.env.DEV) {
20
+ void import('./diagnostics.ts')
21
+ }
22
+
23
+ render(() => <App />, document.querySelector('#root') as HTMLElement)
@@ -0,0 +1,78 @@
1
+ import { members } from './data.ts'
2
+ import { DOCS } from './docs.ts'
3
+
4
+ /**
5
+ * The data view.
6
+ *
7
+ * **The schema is markup and the data is a property.** The columns are
8
+ * elements you can read in the HTML, and `rows` is the whole result handed over
9
+ * at once, since a table of ten thousand rows cannot be written out as
10
+ * elements. The table sorts, pages, and selects out of that array on its own.
11
+ *
12
+ * **Selection is keyed on `row-key`, so it survives a sort and a page.**
13
+ * `app.css` paints the chosen row through the parts the table names for it.
14
+ */
15
+ export function Overview() {
16
+ return (
17
+ <div class="stack">
18
+ <header class="hero">
19
+ <helix-text emphasis="muted" variant="label">
20
+ Data view
21
+ </helix-text>
22
+ <helix-text as="h1" variant="heading">
23
+ Roster
24
+ </helix-text>
25
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
26
+ Twelve people from a local module. Sort a column, turn a page, or tick a row and watch the
27
+ rule in <helix-code>src/app.css</helix-code> paint it.
28
+ </helix-text>
29
+ </header>
30
+
31
+ <section class="section">
32
+ <helix-data-table
33
+ default-sort="name:asc"
34
+ label="Roster"
35
+ page-size={6}
36
+ row-key="id"
37
+ rows={members}
38
+ selectable
39
+ >
40
+ {/* **Every column declares what it may give up.** A floor and a share:
41
+ the floor is what the words need, and the share is how the slack
42
+ is divided. At 390 the four columns and the checkboxes fit the
43
+ box, and below 376 the table scrolls its own box. */}
44
+ <helix-table-column field="name" sortable width="minmax(5.5rem, 1.6fr)">
45
+ Name
46
+ </helix-table-column>
47
+ <helix-table-column field="team" sortable width="minmax(4.5rem, 1fr)">
48
+ Team
49
+ </helix-table-column>
50
+ <helix-table-column field="role" width="minmax(4.5rem, 1fr)">
51
+ Role
52
+ </helix-table-column>
53
+ {/* `end` lines the digits up under each other. `max-content`, since a
54
+ column of numerals has one right width. */}
55
+ <helix-table-column field="reviews" sortable text-align="end" width="max-content">
56
+ Reviews
57
+ </helix-table-column>
58
+
59
+ {/* The footer is a row across the table: anything you slot goes at the
60
+ start, and the table's own pagination sits at the end. */}
61
+ <helix-text emphasis="subtle" slot="footer" variant="caption">
62
+ {members.length} people
63
+ </helix-text>
64
+ </helix-data-table>
65
+
66
+ {/* A footnote to the table, so it takes the section's gap. */}
67
+ <helix-text as="p" emphasis="muted" variant="caption">
68
+ Filtering, column reordering, resizing, virtualized rows and a server mode that asks you
69
+ for a page are all on{' '}
70
+ <a data-link href={DOCS.table}>
71
+ the component's own page
72
+ </a>
73
+ .
74
+ </helix-text>
75
+ </section>
76
+ </div>
77
+ )
78
+ }
@@ -0,0 +1,22 @@
1
+ import { type Accessor, createSignal } from 'solid-js'
2
+
3
+ /* **The whole router, which is a hash and a listener.**
4
+
5
+ A starter should not pick your router for you, and three pages need
6
+ `location.hash` and one event. Swap this file for Solid Router when you want
7
+ nested routes or preloading: its `HashRouter` keeps these addresses.
8
+
9
+ A hash, because the History API wants a server that answers every path with
10
+ the same document, and `vite preview` and wherever you deploy each have to be
11
+ told to. A hash asks nothing of either. Each link in the app is a plain anchor
12
+ to `#/` and a path. */
13
+
14
+ /** Where the reader is, as a path, so the first page is `/`. */
15
+ const read = (): string => window.location.hash.slice(1) || '/'
16
+
17
+ const [path, setPath] = createSignal(read())
18
+
19
+ window.addEventListener('hashchange', () => setPath(read()))
20
+
21
+ /** The current path, kept current as the hash changes. */
22
+ export const pathname: Accessor<string> = path
@@ -0,0 +1,151 @@
1
+ import { createSignal, For, type JSX } from 'solid-js'
2
+ import { teams } from './data.ts'
3
+ import { DOCS } from './docs.ts'
4
+
5
+ /** One message in the toast region, and what it says. */
6
+ interface Message {
7
+ readonly id: number
8
+ readonly text: string
9
+ }
10
+
11
+ /**
12
+ * The form.
13
+ *
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
+ * The submit proves it: it reads the answers with `new FormData(form)`, and
17
+ * nothing gathers them by hand.
18
+ *
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
+ */
23
+ export function SignupForm() {
24
+ const [handle, setHandle] = createSignal('')
25
+ const [messages, setMessages] = createSignal<readonly Message[]>([])
26
+
27
+ const join: JSX.EventHandler<HTMLFormElement, SubmitEvent> = (event) => {
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
+ event.preventDefault()
31
+ const answers = [...new FormData(event.currentTarget).entries()]
32
+ .map(([field, value]) => `${field}=${value}`)
33
+ .join(' ')
34
+ setMessages((sent) => [...sent, { id: Date.now(), text: answers }])
35
+ }
36
+
37
+ return (
38
+ <div class="stack">
39
+ <header class="hero">
40
+ <helix-text emphasis="muted" variant="label">
41
+ Form
42
+ </helix-text>
43
+ <helix-text as="h1" variant="heading">
44
+ Join the roster
45
+ </helix-text>
46
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
47
+ Five controls, and a <helix-code>&lt;form&gt;</helix-code> reads every one of them the way
48
+ it reads an <helix-code>input</helix-code>.
49
+ </helix-text>
50
+ </header>
51
+
52
+ {/* The form takes a column of its own and the notes take what is left.
53
+ At 390 the two stack, and the notes read as a postscript. */}
54
+ <div class="split">
55
+ <form class="form" onSubmit={join}>
56
+ <helix-input autocomplete="name" label="Name" name="name" required />
57
+
58
+ {/* Validation is the platform's: `type` and `required` are set on the
59
+ real input inside, and its own message shows once the reader has
60
+ touched the field. Set `feedback` to say it in your own words. */}
61
+ <helix-input
62
+ hint="Where we send the welcome note."
63
+ label="Email"
64
+ name="email"
65
+ required
66
+ type="email"
67
+ />
68
+
69
+ {/* **A controlled value, which is the one habit to unlearn.** `on:` binds
70
+ the event by its own name. A Helix element is not read-only: writing
71
+ `value` seeds it, and the element goes on applying its own changes.
72
+ So taking the decision means refusing the change with
73
+ `preventDefault()`, then setting the value you want. Leaving
74
+ `handle` as it was does not refuse anything. */}
75
+ <helix-input
76
+ hint="Lowercase and hyphens, whatever you type."
77
+ label="Handle"
78
+ name="handle"
79
+ on:helix-before-change={(event) => {
80
+ event.preventDefault()
81
+ setHandle(event.detail.value.toLowerCase().replaceAll(/[^a-z0-9-]/g, ''))
82
+ }}
83
+ value={handle()}
84
+ />
85
+
86
+ <helix-select label="Team" name="team" placeholder="Pick one" required>
87
+ <For each={teams}>{(team) => <helix-option value={team}>{team}</helix-option>}</For>
88
+ </helix-select>
89
+
90
+ {/* A switch is a thing happening now, which is why it says on and off.
91
+ It still submits, as `on`. */}
92
+ <helix-switch label="Send me the weekly digest" name="digest" />
93
+
94
+ <helix-button emphasis="solid" tone="primary" type="submit">
95
+ Join
96
+ </helix-button>
97
+ </form>
98
+
99
+ <aside class="notes">
100
+ {/* The one fact you need before typing your name into a page you have
101
+ had for five minutes, so it is a callout. `tone` is the meaning,
102
+ and the component draws it with a tint and a bar on the inline
103
+ start, so it survives being read in grayscale. */}
104
+ <helix-callout tone="info">
105
+ Nothing is posted anywhere. Submit it and the message shows exactly what a server would
106
+ have been handed.
107
+ </helix-callout>
108
+ <helix-text as="h2" variant="title">
109
+ What this is showing
110
+ </helix-text>
111
+ <helix-text as="p" emphasis="muted" variant="body">
112
+ Every control is a form-associated custom element, so the submit reads them with{' '}
113
+ <helix-code>new FormData(form)</helix-code> and nothing gathers the answers by hand.
114
+ Reset the form and they all go back, the way an <helix-code>input</helix-code> does.
115
+ </helix-text>
116
+ <helix-text as="p" emphasis="muted" variant="body">
117
+ Leave the email wrong and the complaint is the browser's own, in the reader's language.
118
+ Handle is the controlled one: it refuses the change and writes back what it will accept.
119
+ </helix-text>
120
+ <a data-link href={DOCS.forms}>
121
+ Forms
122
+ </a>
123
+ </aside>
124
+ </div>
125
+
126
+ {/* **The region is rendered whether or not there is anything in it.**
127
+ Assistive technology subscribes to a live region, and one that
128
+ arrives already holding its message is often never announced. A
129
+ dismissed message asks to go through `helix-dismiss`. */}
130
+ <helix-toast>
131
+ <For each={messages()}>
132
+ {(message) => (
133
+ <helix-toast-item
134
+ dismissible
135
+ duration={8000}
136
+ on:helix-dismiss={() => {
137
+ setMessages((sent) => sent.filter((one) => one.id !== message.id))
138
+ }}
139
+ tone="success"
140
+ >
141
+ <helix-flex direction="column" gap="2xs">
142
+ <helix-text variant="body-strong">Read by the form</helix-text>
143
+ <helix-text variant="code">{message.text}</helix-text>
144
+ </helix-flex>
145
+ </helix-toast-item>
146
+ )}
147
+ </For>
148
+ </helix-toast>
149
+ </div>
150
+ )
151
+ }
@@ -0,0 +1,34 @@
1
+ import { DOCS } from './docs.ts'
2
+
3
+ /**
4
+ * The bar's counterpart, on every route.
5
+ *
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. The footer is also where the links you want once belong.
9
+ *
10
+ * `data-link="subtle"` reads as body text until it is hovered, which suits a
11
+ * row of links under a rule.
12
+ */
13
+ export function SiteFooter() {
14
+ return (
15
+ <footer class="site-footer">
16
+ <div class="footer-row">
17
+ <helix-text as="p" emphasis="subtle" variant="caption">
18
+ Every component in here, with its API, its parts and its examples, has a page.
19
+ </helix-text>
20
+ <div class="footer-links">
21
+ <a data-link="subtle" href={DOCS.gallery}>
22
+ Components
23
+ </a>
24
+ <a data-link="subtle" href={DOCS.elements}>
25
+ Documentation
26
+ </a>
27
+ <a data-link="subtle" href={DOCS.solid}>
28
+ Solid
29
+ </a>
30
+ </div>
31
+ </div>
32
+ </footer>
33
+ )
34
+ }