@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,77 @@
1
+ import { createSignal } from 'solid-js'
2
+ import { pathname } from './routes.ts'
3
+
4
+ /** What the page can follow, in the order the control cycles them. */
5
+ const MODES = ['system', 'light', 'dark'] as const
6
+
7
+ type Mode = (typeof MODES)[number]
8
+
9
+ /** The glyph each mode is announced and drawn with. */
10
+ const MARKS: Record<Mode, string> = { dark: 'moon', light: 'sun', system: 'monitor' }
11
+
12
+ /**
13
+ * The bar across the top: the wordmark, the destinations, and the color mode.
14
+ */
15
+ export function SiteHeader() {
16
+ /* `system` is the resting state, so the control draws the same glyph before
17
+ and after the reader first touches it. */
18
+ const [mode, setMode] = createSignal<Mode>('system')
19
+
20
+ /**
21
+ * One attribute, two modes, and no second stylesheet.
22
+ *
23
+ * Every Helix token is written once as a `light-dark()` pair, so
24
+ * `color-scheme` decides which half resolves. `data-color-mode` sets it, and
25
+ * since `color-scheme` inherits, the attribute works on any element: put it
26
+ * on a panel for a light preview inside a dark page.
27
+ *
28
+ * `system` removes the attribute. Writing out whichever mode is current would
29
+ * freeze the page there the moment the reader's machine changed.
30
+ */
31
+ const cycle = () => {
32
+ const next = MODES[(MODES.indexOf(mode()) + 1) % MODES.length] ?? 'system'
33
+ setMode(next)
34
+ if (next === 'system') {
35
+ delete document.documentElement.dataset.colorMode
36
+ return
37
+ }
38
+ document.documentElement.dataset.colorMode = next
39
+ }
40
+
41
+ return (
42
+ <helix-app-header material="glass" sticky>
43
+ {/* The wordmark is the way back to the first page, so there is no third
44
+ destination in the bar saying the same thing. It is also the only name
45
+ of the project on the screen. The mark is a placeholder for your own,
46
+ drawn in `--helix-color-brand-mark`, the token for the brand as a
47
+ graphic. */}
48
+ <a class="wordmark" data-link="quiet" href="#/" slot="start">
49
+ <helix-icon name="cube" />
50
+ <helix-text variant="body-strong">__PROJECT_NAME__</helix-text>
51
+ </a>
52
+
53
+ {/* The links are yours. Navigation owns the marker that travels between
54
+ them and renders no anchors of its own. The base layer styles them,
55
+ `aria-current="page"` included. */}
56
+ <helix-navigation label="Pages">
57
+ <a aria-current={pathname() === '/overview' ? 'page' : undefined} href="#/overview">
58
+ Overview
59
+ </a>
60
+ <a aria-current={pathname() === '/join' ? 'page' : undefined} href="#/join">
61
+ Join
62
+ </a>
63
+ </helix-navigation>
64
+
65
+ {/* An icon with no words has no accessible name until it is given one,
66
+ which is why this is its own element. */}
67
+ <helix-icon-button
68
+ label={`Color mode: ${mode()}`}
69
+ onClick={cycle}
70
+ slot="end"
71
+ title={`Color mode: ${mode()}`}
72
+ >
73
+ <helix-icon name={MARKS[mode()]} />
74
+ </helix-icon-button>
75
+ </helix-app-header>
76
+ )
77
+ }
@@ -0,0 +1,27 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowImportingTsExtensions": true,
4
+ "jsx": "preserve",
5
+ "jsxImportSource": "solid-js",
6
+ "lib": [
7
+ "DOM",
8
+ "DOM.Iterable",
9
+ "ES2023"
10
+ ],
11
+ "module": "ESNext",
12
+ "moduleResolution": "bundler",
13
+ "noEmit": true,
14
+ "resolveJsonModule": true,
15
+ "skipLibCheck": true,
16
+ "strict": true,
17
+ "target": "ES2023",
18
+ "types": [
19
+ "vite/client"
20
+ ],
21
+ "verbatimModuleSyntax": true
22
+ },
23
+ "include": [
24
+ "src",
25
+ "helix-solid.d.ts"
26
+ ]
27
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vite'
2
+ import solid from 'vite-plugin-solid'
3
+
4
+ /* Solid needs no setting for custom elements: it sets a property on one by
5
+ default, `attr:` forces an attribute, and `on:` binds an event by its own
6
+ name. */
7
+ export default defineConfig({ plugins: [solid()] })
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Types for the Helix custom elements in Svelte components.
3
+ *
4
+ * Generated from the same manifest the API pages read, so a wrong prop or a
5
+ * wrong event detail is an error in `svelte-check`. The import augments
6
+ * `SvelteHTMLElements` with the JSX typings, which already describe a handler
7
+ * bound as `onhelix-before-change={…}`.
8
+ */
9
+ import '@fusion.dev/helix/svelte'
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>__PROJECT_NAME__</title>
7
+ </head>
8
+ <body>
9
+ <div id="app"></div>
10
+ <script type="module" src="/src/main.ts"></script>
11
+ </body>
12
+ </html>
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "__PROJECT_NAME__",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "build": "svelte-check && vite build",
8
+ "dev": "vite",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@fusion.dev/helix": "^__HELIX_VERSION__",
13
+ "@fusion.dev/helix-styles": "^__HELIX_VERSION__"
14
+ },
15
+ "devDependencies": {
16
+ "@fusion.dev/helix-diagnostics": "^__HELIX_VERSION__",
17
+ "@sveltejs/vite-plugin-svelte": "^7.3.1",
18
+ "svelte": "^5.57.1",
19
+ "svelte-check": "^4.7.6",
20
+ "typescript": "~6.0.3",
21
+ "vite": "^8.3.0"
22
+ }
23
+ }
@@ -0,0 +1,35 @@
1
+ <!--
2
+ The shell, and the switch between the three pages.
3
+
4
+ Helix components are custom elements, so you write the tags directly. There
5
+ is no wrapper package and nothing to import per component: `src/main.ts`
6
+ registers them once, and every tag in the app is real from then on.
7
+
8
+ The bar, the route, and the footer sit in a column the height of the window.
9
+ `app.css` says why the footer is worth having and why the column is flex.
10
+ -->
11
+ <script lang="ts">
12
+ import Landing from './Landing.svelte'
13
+ import Overview from './Overview.svelte'
14
+ import { route } from './routes.svelte.ts'
15
+ import SignupForm from './SignupForm.svelte'
16
+ import SiteFooter from './SiteFooter.svelte'
17
+ import SiteHeader from './SiteHeader.svelte'
18
+
19
+ /** The page for each path, in the order the bar offers them. */
20
+ const PAGES = {
21
+ '/': Landing,
22
+ '/join': SignupForm,
23
+ '/overview': Overview,
24
+ }
25
+
26
+ const Page = $derived(PAGES[route.path as keyof typeof PAGES] ?? Landing)
27
+ </script>
28
+
29
+ <div class="shell">
30
+ <SiteHeader />
31
+ <main class="page">
32
+ <Page />
33
+ </main>
34
+ <SiteFooter />
35
+ </div>
@@ -0,0 +1,137 @@
1
+ <!--
2
+ The first screen, and the only page here about the starter itself.
3
+
4
+ Delete it once you know your way around: nothing else imports it, and the
5
+ route it answers becomes yours.
6
+
7
+ **The heading says what this is, and the project's name stays in the bar.**
8
+
9
+ **The calls to action are anchors.** A button that navigates is a link
10
+ dressed as a button, and the two are different things to a keyboard and to
11
+ a screen reader. The base stylesheet styles an anchor that asks with
12
+ `data-button`, so a link can look like one and stay a link.
13
+ -->
14
+ <script lang="ts">
15
+ import { roster } from './data.ts'
16
+ import { DOCS } from './docs.ts'
17
+
18
+ /* **A locale named, so the number reads the same everywhere.**
19
+ `toLocaleString()` with no argument reads the machine's locale, so the same
20
+ number is grouped two ways on two machines. */
21
+ const figure = new Intl.NumberFormat('en-US')
22
+ </script>
23
+
24
+ <div class="stack">
25
+ <header class="first-screen">
26
+ <div class="hero">
27
+ <helix-text emphasis="muted" variant="label">Helix starter</helix-text>
28
+ <helix-text as="h1">Every tag on this page is Helix</helix-text>
29
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
30
+ Nothing here is a wrapper: the tags are the library's own elements, and every color,
31
+ size and space inside them comes from one set of tokens. Three routes are laid down, and
32
+ all of them are yours.
33
+ </helix-text>
34
+ <div class="actions">
35
+ <a data-button="solid" data-button-tone="primary" href="#/overview">See the data view</a>
36
+ <a data-button href="#/join">Open the form</a>
37
+ </div>
38
+ </div>
39
+
40
+ <!-- The figures are the roster the data view draws, read from the same
41
+ module, so the panel is the app at work. `helix-stat` sets the number
42
+ on the display face at a size the type scale does not reach. -->
43
+ <helix-card class="specimen" separation="outlined">
44
+ <helix-text as="h2" slot="header" variant="title">The roster, in three figures</helix-text>
45
+ <helix-stat-group divided>
46
+ <helix-stat>
47
+ {roster.people}
48
+ <span slot="label">People</span>
49
+ </helix-stat>
50
+ <helix-stat>
51
+ {roster.teams}
52
+ <span slot="label">Teams</span>
53
+ </helix-stat>
54
+ <helix-stat>
55
+ {figure.format(roster.reviews)}
56
+ <span slot="label">Reviews</span>
57
+ </helix-stat>
58
+ </helix-stat-group>
59
+ <a data-link href="#/overview" slot="footer">Open the data view</a>
60
+ </helix-card>
61
+ </header>
62
+
63
+ <section class="section">
64
+ <helix-text as="h2" variant="subheading">What you have</helix-text>
65
+ <!-- Three paragraphs about one installed thing, so they take a rule each
66
+ and no box. `app.css` says why at more length. -->
67
+ <div class="facts">
68
+ <article class="fact">
69
+ <helix-text as="h3" variant="title">Elements, not wrappers</helix-text>
70
+ <helix-text as="p" emphasis="muted" variant="body">
71
+ They are custom elements, so you write the tags and there is no per-component import
72
+ to keep in step. One line in <helix-code>src/main.ts</helix-code> registers all of
73
+ them, and a subpath like <helix-code>@fusion.dev/helix/tabs</helix-code> registers
74
+ one.
75
+ </helix-text>
76
+ <a data-link href={DOCS.elements}>Using the elements</a>
77
+ </article>
78
+
79
+ <article class="fact">
80
+ <helix-text as="h3" variant="title">One set of tokens</helix-text>
81
+ <helix-text as="p" emphasis="muted" variant="body">
82
+ Every color is written once as a light and dark pair, so the control in the bar moves
83
+ the whole page by setting a single attribute on the root. Type, spacing, radii and
84
+ control sizes come from the same place.
85
+ </helix-text>
86
+ <a data-link href={DOCS.tokens}>Tokens</a>
87
+ </article>
88
+
89
+ <article class="fact">
90
+ <helix-text as="h3" variant="title">A short stylesheet</helix-text>
91
+ <helix-text as="p" emphasis="muted" variant="body">
92
+ <helix-code>src/app.css</helix-code> holds this page's measure, the rhythm between its
93
+ blocks, and nothing a component already draws. Where a component leaves a decision
94
+ open it names a part: the data view reaches one to paint the row you tick.
95
+ </helix-text>
96
+ <a data-link href={DOCS.parts}>Overriding a component</a>
97
+ </article>
98
+ </div>
99
+ </section>
100
+
101
+ <section class="section">
102
+ <helix-text as="h2" variant="subheading">Where to go next</helix-text>
103
+ <!-- **The whole card is the link, which is what `data-card` is for.** The
104
+ base layer draws the ring, moves the card's border to the brand on
105
+ hover, and leaves focus alone. Nothing inside may be interactive,
106
+ since a link inside a link is neither valid nor reachable. -->
107
+ <helix-grid columns={2} gap="md">
108
+ <a data-card href="#/overview">
109
+ <helix-card separation="outlined">
110
+ <helix-flex align-items="baseline" gap="sm" justify-content="between" slot="header">
111
+ <helix-text as="h3" variant="title">The data view</helix-text>
112
+ <helix-text emphasis="subtle" variant="code">/overview</helix-text>
113
+ </helix-flex>
114
+ <helix-text as="p" emphasis="muted" variant="body">
115
+ Twelve people from a local module, sorted, paged and selected with nothing wired up.
116
+ The columns are markup you can read and the rows are one property, which is what
117
+ keeps the same component working at ten thousand.
118
+ </helix-text>
119
+ </helix-card>
120
+ </a>
121
+
122
+ <a data-card href="#/join">
123
+ <helix-card separation="outlined">
124
+ <helix-flex align-items="baseline" gap="sm" justify-content="between" slot="header">
125
+ <helix-text as="h3" variant="title">The form</helix-text>
126
+ <helix-text emphasis="subtle" variant="code">/join</helix-text>
127
+ </helix-flex>
128
+ <helix-text as="p" emphasis="muted" variant="body">
129
+ Five controls a <helix-code>&lt;form&gt;</helix-code> reads with
130
+ <helix-code>FormData</helix-code>, because every one of them is a real form
131
+ participant. The email field's complaint is the browser's own.
132
+ </helix-text>
133
+ </helix-card>
134
+ </a>
135
+ </helix-grid>
136
+ </section>
137
+ </div>
@@ -0,0 +1,67 @@
1
+ <!--
2
+ The data view.
3
+
4
+ **The schema is markup and the data is a property.** The columns are
5
+ elements you can read in the HTML, and `rows` is the whole result handed over
6
+ at once, since a table of ten thousand rows cannot be written out as
7
+ elements. The table sorts, pages, and selects out of that array on its own.
8
+
9
+ **Selection is keyed on `row-key`, so it survives a sort and a page.**
10
+ `app.css` paints the chosen row through the parts the table names for it.
11
+ -->
12
+ <script lang="ts">
13
+ import { members } from './data.ts'
14
+ import { DOCS } from './docs.ts'
15
+ </script>
16
+
17
+ <div class="stack">
18
+ <header class="hero">
19
+ <helix-text emphasis="muted" variant="label">Data view</helix-text>
20
+ <helix-text as="h1" variant="heading">Roster</helix-text>
21
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
22
+ Twelve people from a local module. Sort a column, turn a page, or tick a row and watch the
23
+ rule in <helix-code>src/app.css</helix-code> paint it.
24
+ </helix-text>
25
+ </header>
26
+
27
+ <section class="section">
28
+ <helix-data-table
29
+ default-sort="name:asc"
30
+ label="Roster"
31
+ page-size={6}
32
+ row-key="id"
33
+ rows={members}
34
+ selectable
35
+ >
36
+ <!-- **Every column declares what it may give up.** A floor and a share:
37
+ the floor is what the words need, and the share is how the slack is
38
+ divided. At 390 the four columns and the checkboxes fit the box, and
39
+ below 376 the table scrolls its own box. -->
40
+ <helix-table-column field="name" sortable width="minmax(5.5rem, 1.6fr)">
41
+ Name
42
+ </helix-table-column>
43
+ <helix-table-column field="team" sortable width="minmax(4.5rem, 1fr)">
44
+ Team
45
+ </helix-table-column>
46
+ <helix-table-column field="role" width="minmax(4.5rem, 1fr)">Role</helix-table-column>
47
+ <!-- `end` lines the digits up under each other. `max-content`, since a
48
+ column of numerals has one right width. -->
49
+ <helix-table-column field="reviews" sortable text-align="end" width="max-content">
50
+ Reviews
51
+ </helix-table-column>
52
+
53
+ <!-- The footer is a row across the table: anything you slot goes at the
54
+ start, and the table's own pagination sits at the end. -->
55
+ <helix-text emphasis="subtle" slot="footer" variant="caption">
56
+ {members.length} people
57
+ </helix-text>
58
+ </helix-data-table>
59
+
60
+ <!-- A footnote to the table, so it takes the section's gap. -->
61
+ <helix-text as="p" emphasis="muted" variant="caption">
62
+ Filtering, column reordering, resizing, virtualized rows and a server mode that asks you
63
+ for a page are all on
64
+ <a data-link href={DOCS.table}>the component's own page</a>.
65
+ </helix-text>
66
+ </section>
67
+ </div>
@@ -0,0 +1,148 @@
1
+ <!--
2
+ The form.
3
+
4
+ **Every control here is a form-associated custom element**, so a `<form>`
5
+ collects them, resets them, and validates them the way it does an `<input>`.
6
+ The submit proves it: it reads the answers with `new FormData(form)`, and
7
+ nothing gathers them by hand.
8
+
9
+ There is no wrapping `Field` component. A label in one shadow root cannot
10
+ name a control in another, so every control renders its own `label`, `hint`,
11
+ and `feedback`.
12
+ -->
13
+ <script lang="ts">
14
+ import type { HelixInputHelixBeforeChangeElementEvent } from '@fusion.dev/helix/jsx'
15
+ import { teams } from './data.ts'
16
+ import { DOCS } from './docs.ts'
17
+
18
+ /** One message in the toast region, and what it says. */
19
+ interface Message {
20
+ readonly id: number
21
+ readonly text: string
22
+ }
23
+
24
+ let handle = $state('')
25
+ let messages = $state<readonly Message[]>([])
26
+
27
+ /**
28
+ * The page takes the submit, so nothing navigates. Remove `preventDefault()`
29
+ * and the form posts to the server like any other, with the same names.
30
+ */
31
+ const join = (event: SubmitEvent) => {
32
+ event.preventDefault()
33
+ const answers = [...new FormData(event.currentTarget as HTMLFormElement).entries()]
34
+ .map(([field, value]) => `${field}=${value}`)
35
+ .join(' ')
36
+ messages = [...messages, { id: Date.now(), text: answers }]
37
+ }
38
+
39
+ /**
40
+ * **A controlled value, which is the one habit to unlearn.** A Helix element
41
+ * is not read-only: writing `value` seeds it, and the element goes on
42
+ * applying its own changes. So taking the decision means refusing the change
43
+ * with `preventDefault()`, then setting the value you want. Leaving `handle`
44
+ * as it was does not refuse anything.
45
+ */
46
+ const takeHandle = (event: HelixInputHelixBeforeChangeElementEvent) => {
47
+ event.preventDefault()
48
+ handle = event.detail.value.toLowerCase().replaceAll(/[^a-z0-9-]/g, '')
49
+ }
50
+
51
+ const dismiss = (id: number) => {
52
+ messages = messages.filter((one) => one.id !== id)
53
+ }
54
+ </script>
55
+
56
+ <div class="stack">
57
+ <header class="hero">
58
+ <helix-text emphasis="muted" variant="label">Form</helix-text>
59
+ <helix-text as="h1" variant="heading">Join the roster</helix-text>
60
+ <helix-text as="p" class="lede" emphasis="muted" variant="body">
61
+ Five controls, and a <helix-code>&lt;form&gt;</helix-code> reads every one of them the way
62
+ it reads an <helix-code>input</helix-code>.
63
+ </helix-text>
64
+ </header>
65
+
66
+ <!-- The form takes a column of its own and the notes take what is left. At
67
+ 390 the two stack, and the notes read as a postscript. -->
68
+ <div class="split">
69
+ <form class="form" onsubmit={join}>
70
+ <helix-input autocomplete="name" label="Name" name="name" required></helix-input>
71
+
72
+ <!-- Validation is the platform's: `type` and `required` are set on the
73
+ real input inside, and its own message shows once the reader has
74
+ touched the field. Set `feedback` to say it in your own words. -->
75
+ <helix-input
76
+ hint="Where we send the welcome note."
77
+ label="Email"
78
+ name="email"
79
+ required
80
+ type="email"
81
+ ></helix-input>
82
+
83
+ <!-- `onhelix-before-change` binds with `addEventListener`, so it takes
84
+ the event's own name. -->
85
+ <helix-input
86
+ hint="Lowercase and hyphens, whatever you type."
87
+ label="Handle"
88
+ name="handle"
89
+ value={handle}
90
+ onhelix-before-change={takeHandle}
91
+ ></helix-input>
92
+
93
+ <helix-select label="Team" name="team" placeholder="Pick one" required>
94
+ {#each teams as team (team)}
95
+ <helix-option value={team}>{team}</helix-option>
96
+ {/each}
97
+ </helix-select>
98
+
99
+ <!-- A switch is a thing happening now, which is why it says on and off.
100
+ It still submits, as `on`. -->
101
+ <helix-switch label="Send me the weekly digest" name="digest"></helix-switch>
102
+
103
+ <helix-button emphasis="solid" tone="primary" type="submit">Join</helix-button>
104
+ </form>
105
+
106
+ <aside class="notes">
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 and a bar on the inline
110
+ start, so it survives being read in grayscale. -->
111
+ <helix-callout tone="info">
112
+ Nothing is posted anywhere. Submit it and the message shows exactly what a server would
113
+ have been handed.
114
+ </helix-callout>
115
+ <helix-text as="h2" variant="title">What this is showing</helix-text>
116
+ <helix-text as="p" emphasis="muted" variant="body">
117
+ Every control is a form-associated custom element, so the submit reads them with
118
+ <helix-code>new FormData(form)</helix-code> and nothing gathers the answers by hand.
119
+ Reset the form and they all go back, the way an <helix-code>input</helix-code> does.
120
+ </helix-text>
121
+ <helix-text as="p" emphasis="muted" variant="body">
122
+ Leave the email wrong and the complaint is the browser's own, in the reader's language.
123
+ Handle is the controlled one: it refuses the change and writes back what it will accept.
124
+ </helix-text>
125
+ <a data-link href={DOCS.forms}>Forms</a>
126
+ </aside>
127
+ </div>
128
+
129
+ <!-- **The region is rendered whether or not there is anything in it.**
130
+ Assistive technology subscribes to a live region, and one that arrives
131
+ already holding its message is often never announced. A dismissed
132
+ message asks to go through `helix-dismiss`. -->
133
+ <helix-toast>
134
+ {#each messages as message (message.id)}
135
+ <helix-toast-item
136
+ dismissible
137
+ duration={8000}
138
+ onhelix-dismiss={() => dismiss(message.id)}
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
+ {/each}
147
+ </helix-toast>
148
+ </div>
@@ -0,0 +1,26 @@
1
+ <!--
2
+ The bar's counterpart, on every route.
3
+
4
+ **It puts a floor under the content.** The overview is a heading, a table,
5
+ and a caption, which on a tall window left the bottom third of the screen
6
+ empty. The footer is also where the links you want once belong.
7
+
8
+ `data-link="subtle"` reads as body text until it is hovered, which suits a
9
+ row of links under a rule.
10
+ -->
11
+ <script lang="ts">
12
+ import { DOCS } from './docs.ts'
13
+ </script>
14
+
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}>Components</a>
22
+ <a data-link="subtle" href={DOCS.elements}>Documentation</a>
23
+ <a data-link="subtle" href={DOCS.svelte}>Svelte</a>
24
+ </div>
25
+ </div>
26
+ </footer>
@@ -0,0 +1,72 @@
1
+ <!-- The bar across the top: the wordmark, the destinations, and the color mode. -->
2
+ <script lang="ts">
3
+ import { route } from './routes.svelte.ts'
4
+
5
+ /** What the page can follow, in the order the control cycles them. */
6
+ const MODES = ['system', 'light', 'dark'] as const
7
+
8
+ type Mode = (typeof MODES)[number]
9
+
10
+ /** The glyph each mode is announced and drawn with. */
11
+ const MARKS: Record<Mode, string> = { dark: 'moon', light: 'sun', system: 'monitor' }
12
+
13
+ /* `system` is the resting state, so the control draws the same glyph before
14
+ and after the reader first touches it. */
15
+ let mode = $state<Mode>('system')
16
+
17
+ /**
18
+ * One attribute, two modes, and no second stylesheet.
19
+ *
20
+ * Every Helix token is written once as a `light-dark()` pair, so
21
+ * `color-scheme` decides which half resolves. `data-color-mode` sets it, and
22
+ * since `color-scheme` inherits, the attribute works on any element: put it
23
+ * on a panel for a light preview inside a dark page.
24
+ *
25
+ * `system` removes the attribute. Writing out whichever mode is current would
26
+ * freeze the page there the moment the reader's machine changed.
27
+ */
28
+ const cycle = () => {
29
+ const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length] ?? 'system'
30
+ mode = next
31
+ if (next === 'system') {
32
+ delete document.documentElement.dataset.colorMode
33
+ return
34
+ }
35
+ document.documentElement.dataset.colorMode = next
36
+ }
37
+ </script>
38
+
39
+ <helix-app-header material="glass" sticky>
40
+ <!-- The wordmark is the way back to the first page, so there is no third
41
+ destination in the bar saying the same thing. It is also the only name of
42
+ the project on the screen. The mark is a placeholder for your own, drawn
43
+ in `--helix-color-brand-mark`, the token for the brand as a graphic. -->
44
+ <a class="wordmark" data-link="quiet" href="#/" slot="start">
45
+ <helix-icon name="cube"></helix-icon>
46
+ <helix-text variant="body-strong">__PROJECT_NAME__</helix-text>
47
+ </a>
48
+
49
+ <!-- The links are yours. Navigation owns the marker that travels between them
50
+ and renders no anchors of its own. The base layer styles them,
51
+ `aria-current="page"` included. -->
52
+ <helix-navigation label="Pages">
53
+ <a aria-current={route.path === '/overview' ? 'page' : undefined} href="#/overview">Overview</a>
54
+ <a aria-current={route.path === '/join' ? 'page' : undefined} href="#/join">Join</a>
55
+ </helix-navigation>
56
+
57
+ <!-- An icon with no words has no accessible name until it is given one, which
58
+ is why this is its own element.
59
+
60
+ Svelte's accessibility check reads the tag, and the button is inside the
61
+ element's shadow root, with its role and its keys. So the two warnings
62
+ about a click on an element with neither are ignored here. -->
63
+ <!-- svelte-ignore a11y_click_events_have_key_events, a11y_no_static_element_interactions -->
64
+ <helix-icon-button
65
+ label={`Color mode: ${mode}`}
66
+ onclick={cycle}
67
+ slot="end"
68
+ title={`Color mode: ${mode}`}
69
+ >
70
+ <helix-icon name={MARKS[mode]}></helix-icon>
71
+ </helix-icon-button>
72
+ </helix-app-header>