@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.
Files changed (106) 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 +77 -0
  12. package/templates/angular/src/app/landing.ts +29 -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 +90 -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 +57 -0
  22. package/templates/angular/src/app.css +355 -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/app.css +144 -152
  29. package/templates/react-nextjs/app/data.ts +17 -17
  30. package/templates/react-nextjs/app/diagnostics.ts +19 -0
  31. package/templates/react-nextjs/app/docs.ts +9 -9
  32. package/templates/react-nextjs/app/landing.tsx +63 -121
  33. package/templates/react-nextjs/app/overview.tsx +25 -27
  34. package/templates/react-nextjs/app/signup-form.tsx +36 -39
  35. package/templates/react-nextjs/app/site-footer.tsx +7 -10
  36. package/templates/react-nextjs/app/site-header.tsx +17 -21
  37. package/templates/react-nextjs/helix-jsx.d.ts +5 -7
  38. package/templates/react-nextjs/instrumentation-client.ts +6 -0
  39. package/templates/react-nextjs/package.json +1 -0
  40. package/templates/react-nextjs/tsconfig.json +1 -0
  41. package/templates/react-vite/helix-jsx.d.ts +5 -7
  42. package/templates/react-vite/package.json +1 -0
  43. package/templates/react-vite/src/app.css +144 -152
  44. package/templates/react-vite/src/data.ts +17 -17
  45. package/templates/react-vite/src/diagnostics.ts +24 -0
  46. package/templates/react-vite/src/docs.ts +9 -9
  47. package/templates/react-vite/src/landing.tsx +63 -121
  48. package/templates/react-vite/src/main.tsx +6 -0
  49. package/templates/react-vite/src/overview.tsx +24 -26
  50. package/templates/react-vite/src/routes.tsx +11 -13
  51. package/templates/react-vite/src/signup-form.tsx +36 -39
  52. package/templates/react-vite/src/site-footer.tsx +7 -10
  53. package/templates/react-vite/src/site-header.tsx +15 -19
  54. package/templates/react-vite/tsconfig.json +1 -0
  55. package/templates/solid-vite/helix-solid.d.ts +14 -0
  56. package/templates/solid-vite/index.html +12 -0
  57. package/templates/solid-vite/package.json +22 -0
  58. package/templates/solid-vite/src/App.tsx +35 -0
  59. package/templates/solid-vite/src/app.css +355 -0
  60. package/templates/solid-vite/src/data.ts +81 -0
  61. package/templates/solid-vite/src/diagnostics.ts +24 -0
  62. package/templates/solid-vite/src/docs.ts +29 -0
  63. package/templates/solid-vite/src/landing.tsx +130 -0
  64. package/templates/solid-vite/src/main.tsx +23 -0
  65. package/templates/solid-vite/src/overview.tsx +78 -0
  66. package/templates/solid-vite/src/routes.ts +22 -0
  67. package/templates/solid-vite/src/signup-form.tsx +151 -0
  68. package/templates/solid-vite/src/site-footer.tsx +34 -0
  69. package/templates/solid-vite/src/site-header.tsx +76 -0
  70. package/templates/solid-vite/tsconfig.json +27 -0
  71. package/templates/solid-vite/vite.config.ts +7 -0
  72. package/templates/svelte-vite/helix-svelte.d.ts +9 -0
  73. package/templates/svelte-vite/index.html +12 -0
  74. package/templates/svelte-vite/package.json +23 -0
  75. package/templates/svelte-vite/src/App.svelte +35 -0
  76. package/templates/svelte-vite/src/Landing.svelte +100 -0
  77. package/templates/svelte-vite/src/Overview.svelte +67 -0
  78. package/templates/svelte-vite/src/SignupForm.svelte +147 -0
  79. package/templates/svelte-vite/src/SiteFooter.svelte +26 -0
  80. package/templates/svelte-vite/src/SiteHeader.svelte +71 -0
  81. package/templates/svelte-vite/src/app.css +355 -0
  82. package/templates/svelte-vite/src/data.ts +81 -0
  83. package/templates/svelte-vite/src/diagnostics.ts +24 -0
  84. package/templates/svelte-vite/src/docs.ts +29 -0
  85. package/templates/svelte-vite/src/main.ts +23 -0
  86. package/templates/svelte-vite/src/routes.svelte.ts +19 -0
  87. package/templates/svelte-vite/tsconfig.json +28 -0
  88. package/templates/svelte-vite/vite.config.ts +7 -0
  89. package/templates/vue-vite/helix-vue.d.ts +8 -0
  90. package/templates/vue-vite/index.html +12 -0
  91. package/templates/vue-vite/package.json +23 -0
  92. package/templates/vue-vite/src/App.vue +39 -0
  93. package/templates/vue-vite/src/LandingPage.vue +102 -0
  94. package/templates/vue-vite/src/OverviewPage.vue +69 -0
  95. package/templates/vue-vite/src/SignupForm.vue +149 -0
  96. package/templates/vue-vite/src/SiteFooter.vue +28 -0
  97. package/templates/vue-vite/src/SiteHeader.vue +71 -0
  98. package/templates/vue-vite/src/app.css +355 -0
  99. package/templates/vue-vite/src/data.ts +81 -0
  100. package/templates/vue-vite/src/diagnostics.ts +24 -0
  101. package/templates/vue-vite/src/docs.ts +29 -0
  102. package/templates/vue-vite/src/main.ts +23 -0
  103. package/templates/vue-vite/src/routes.ts +26 -0
  104. package/templates/vue-vite/tsconfig.json +26 -0
  105. package/templates/vue-vite/vite.config.ts +13 -0
  106. package/templates.json +41 -0
@@ -0,0 +1,130 @@
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 calls to action are anchors.** A button that navigates is a link
18
+ * dressed as a button, and the two are different things to a keyboard and to
19
+ * a screen reader. The base stylesheet styles an anchor that asks with
20
+ * `data-button`, so a link can look like one and stay a link.
21
+ */
22
+ export function Landing() {
23
+ return (
24
+ <div class="stack">
25
+ <header class="first-screen">
26
+ {/* **The light behind the first screen**: a helix, drawn by the library
27
+ in the theme's colors, so a new brand color relights it. It shows
28
+ one still frame where motion is reduced, and a gradient where the
29
+ browser draws without a GPU. */}
30
+ <helix-aura class="first-light" data-layer speed={0.4} terminus={0.8} variant="braid" />
31
+ <div class="hero">
32
+ <helix-tag emphasis="subtle" tone="primary">
33
+ Starter
34
+ </helix-tag>
35
+ <helix-text as="h1" variant="display">
36
+ Helix design system
37
+ </helix-text>
38
+ <helix-text as="p" class="lede" variant="body">
39
+ Web components in your brand's colors, drawn from one set of tokens.
40
+ </helix-text>
41
+ <div class="actions">
42
+ <a data-button="solid" data-button-tone="primary" href="#/overview">
43
+ See the data view
44
+ </a>
45
+ <a data-button href="#/join">
46
+ Open the form
47
+ </a>
48
+ </div>
49
+ </div>
50
+
51
+ {/* **The library at work, on the first screen.** The figures are the
52
+ roster the data view draws, read from the same module, so the panel
53
+ is the app itself, on glass over the light. */}
54
+ <helix-card class="specimen" material="glass" separation="outlined">
55
+ <helix-text as="h2" slot="header" variant="title">
56
+ The roster, in three figures
57
+ </helix-text>
58
+ <helix-stat-group divided>
59
+ <helix-stat>
60
+ {roster.people}
61
+ <span slot="label">People</span>
62
+ </helix-stat>
63
+ <helix-stat>
64
+ {roster.teams}
65
+ <span slot="label">Teams</span>
66
+ </helix-stat>
67
+ <helix-stat>
68
+ {figure.format(roster.reviews)}
69
+ <span slot="label">Reviews</span>
70
+ </helix-stat>
71
+ </helix-stat-group>
72
+ <a data-link href="#/overview" slot="footer">
73
+ Open the data view
74
+ </a>
75
+ </helix-card>
76
+ </header>
77
+
78
+ <section class="section">
79
+ <helix-text as="h2" variant="subheading">
80
+ What you have
81
+ </helix-text>
82
+ {/* **Each tile is a link, which is what `data-card` is for.** The base
83
+ layer draws the ring, moves the card's border to the brand on hover,
84
+ and leaves focus alone. Nothing inside may be interactive, since a
85
+ link inside a link is neither valid nor reachable. */}
86
+ <div class="features">
87
+ <a class="feature" data-card href={DOCS.elements}>
88
+ <helix-card separation="outlined">
89
+ <span class="feature-mark">
90
+ <helix-icon name="cube" />
91
+ </span>
92
+ <helix-text as="h3" variant="title">
93
+ Custom elements
94
+ </helix-text>
95
+ <helix-text as="p" emphasis="muted" variant="body">
96
+ One import in <helix-code>src/main.tsx</helix-code> registers every tag.
97
+ </helix-text>
98
+ </helix-card>
99
+ </a>
100
+ <a class="feature" data-card href={DOCS.tokens}>
101
+ <helix-card separation="outlined">
102
+ <span class="feature-mark">
103
+ <helix-icon name="palette" />
104
+ </span>
105
+ <helix-text as="h3" variant="title">
106
+ One set of tokens
107
+ </helix-text>
108
+ <helix-text as="p" emphasis="muted" variant="body">
109
+ Every color, size, and space, in light and dark.
110
+ </helix-text>
111
+ </helix-card>
112
+ </a>
113
+ <a class="feature" data-card href={DOCS.parts}>
114
+ <helix-card separation="outlined">
115
+ <span class="feature-mark">
116
+ <helix-icon name="sliders-horizontal" />
117
+ </span>
118
+ <helix-text as="h3" variant="title">
119
+ Parts to style
120
+ </helix-text>
121
+ <helix-text as="p" emphasis="muted" variant="body">
122
+ Reach inside a component with <helix-code>::part()</helix-code>.
123
+ </helix-text>
124
+ </helix-card>
125
+ </a>
126
+ </div>
127
+ </section>
128
+ </div>
129
+ )
130
+ }
@@ -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 for large
69
+ results are 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
+ Three pages need `location.hash` and one event, so the starter leaves the
6
+ router to you. Swap this file for Solid Router when you want nested routes or
7
+ 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
12
+ anchor to `#/` and a path. */
13
+
14
+ /** The current path, read from the hash, 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 field has been
60
+ touched. 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
+ The form posts nothing. Submit it, and a message shows exactly what a server would
106
+ receive.
107
+ </helix-callout>
108
+ <helix-text as="h2" variant="title">
109
+ What the form shows
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 all
113
+ with <helix-code>new FormData(form)</helix-code>. Reset the form and they all go back,
114
+ 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 browser shows its own message, in your language. Handle is
118
+ the controlled field: the page refuses each change and writes back a cleaned value.
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 here has a page, with its API, its parts, and its examples.
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
+ }
@@ -0,0 +1,76 @@
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, the mode the page follows until the control is pressed. */
17
+ const [mode, setMode] = createSignal<Mode>('system')
18
+
19
+ /**
20
+ * One attribute, two modes, and no second stylesheet.
21
+ *
22
+ * Every Helix token is written once as a `light-dark()` pair, so
23
+ * `color-scheme` decides which half resolves. `data-color-mode` sets it, and
24
+ * since `color-scheme` inherits, the attribute works on any element: put it
25
+ * on a panel for a light preview inside a dark page.
26
+ *
27
+ * `system` removes the attribute. Writing out whichever mode is current would
28
+ * hold the page there when the system's own setting changed.
29
+ */
30
+ const cycle = () => {
31
+ const next = MODES[(MODES.indexOf(mode()) + 1) % MODES.length] ?? 'system'
32
+ setMode(next)
33
+ if (next === 'system') {
34
+ delete document.documentElement.dataset.colorMode
35
+ return
36
+ }
37
+ document.documentElement.dataset.colorMode = next
38
+ }
39
+
40
+ return (
41
+ <helix-app-header material="glass" sticky>
42
+ {/* The wordmark is the way back to the first page, so there is no third
43
+ destination in the bar saying the same thing. It is also the only name
44
+ of the project on the screen. The mark is a placeholder for your own,
45
+ drawn in `--helix-color-brand-mark`, the token for the brand as a
46
+ graphic. */}
47
+ <a class="wordmark" data-link="quiet" href="#/" slot="start">
48
+ <helix-icon name="cube" />
49
+ <helix-text variant="body-strong">__PROJECT_NAME__</helix-text>
50
+ </a>
51
+
52
+ {/* The links are yours. Navigation owns the marker that travels between
53
+ them and renders no anchors of its own. The base layer styles them,
54
+ `aria-current="page"` included. */}
55
+ <helix-navigation label="Pages">
56
+ <a aria-current={pathname() === '/overview' ? 'page' : undefined} href="#/overview">
57
+ Overview
58
+ </a>
59
+ <a aria-current={pathname() === '/join' ? 'page' : undefined} href="#/join">
60
+ Join
61
+ </a>
62
+ </helix-navigation>
63
+
64
+ {/* An icon with no words has no accessible name until it is given one,
65
+ which is why this is its own element. */}
66
+ <helix-icon-button
67
+ label={`Color mode: ${mode()}`}
68
+ onClick={cycle}
69
+ slot="end"
70
+ title={`Color mode: ${mode()}`}
71
+ >
72
+ <helix-icon name={MARKS[mode()]} />
73
+ </helix-icon-button>
74
+ </helix-app-header>
75
+ )
76
+ }
@@ -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>