@fusion.dev/helix-templates 0.34.2 → 0.35.1

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.
@@ -0,0 +1,95 @@
1
+ 'use client'
2
+
3
+ /*
4
+ * **A handler is what makes this a client component**, and so does `usePathname`:
5
+ * a server component's output is serialized, and a function cannot be. The bar
6
+ * is rendered from the layout, so it survives a navigation rather than being
7
+ * rebuilt on every page.
8
+ */
9
+
10
+ import Link from 'next/link'
11
+ import { usePathname } from 'next/navigation'
12
+ import { useState } from 'react'
13
+
14
+ /** What the page can follow, in the order the control cycles them. */
15
+ const MODES = ['system', 'light', 'dark'] as const
16
+
17
+ type Mode = (typeof MODES)[number]
18
+
19
+ /** The glyph each mode is announced and drawn with. */
20
+ const MARKS: Record<Mode, string> = { dark: 'moon', light: 'sun', system: 'monitor' }
21
+
22
+ /**
23
+ * The bar across the top: the wordmark, the destinations, and the color mode.
24
+ */
25
+ export function SiteHeader() {
26
+ const path = usePathname()
27
+ /* `system` is the resting state and what the markup is served in, so the
28
+ control draws the same glyph on the server and on the first client paint. */
29
+ const [mode, setMode] = useState<Mode>('system')
30
+
31
+ /**
32
+ * One attribute, two modes, and no second stylesheet.
33
+ *
34
+ * Every Helix token is written once as a `light-dark()` pair, so which half
35
+ * resolves is decided by `color-scheme` and nothing else. `data-color-mode`
36
+ * sets it, and because `color-scheme` inherits the attribute works on any
37
+ * element, not only on the root: put it on a panel for a light preview inside
38
+ * a dark page.
39
+ *
40
+ * `system` removes the attribute rather than writing out whichever mode is
41
+ * current, which would freeze the page there the moment the reader's machine
42
+ * changed.
43
+ */
44
+ const cycle = () => {
45
+ const next = MODES[(MODES.indexOf(mode) + 1) % MODES.length]
46
+ setMode(next)
47
+ if (next === 'system') {
48
+ delete document.documentElement.dataset.colorMode
49
+ return
50
+ }
51
+ document.documentElement.dataset.colorMode = next
52
+ }
53
+
54
+ return (
55
+ <helix-app-header material="glass" sticky>
56
+ {/* The wordmark is the way back to the first page, which is what a reader
57
+ expects of one, so there is no third destination in the bar saying the
58
+ same thing. It is also the only name of the project on the screen: the
59
+ landing page leads with what this is rather than with what you called
60
+ it.
61
+
62
+ The mark is a placeholder for your own, and it is drawn in
63
+ `--helix-color-brand-mark`, the token for the brand as a graphic. */}
64
+ <Link className="wordmark" data-link="quiet" href="/" slot="start">
65
+ <helix-icon name="cube" />
66
+ <helix-text variant="body-strong">__PROJECT_NAME__</helix-text>
67
+ </Link>
68
+
69
+ {/* The links are yours. Navigation owns the marker that travels between
70
+ them and renders no anchors of its own, because a router renders its
71
+ own and could not render them inside this shadow root. The base layer
72
+ styles them, `aria-current="page"` included. */}
73
+ <helix-navigation label="Pages">
74
+ <Link aria-current={path === '/overview' ? 'page' : undefined} href="/overview">
75
+ Overview
76
+ </Link>
77
+ <Link aria-current={path === '/join' ? 'page' : undefined} href="/join">
78
+ Join
79
+ </Link>
80
+ </helix-navigation>
81
+
82
+ {/* An icon with no words has no accessible name unless it is given one,
83
+ which is the whole reason this is its own element rather than a button
84
+ with a glyph in it. */}
85
+ <helix-icon-button
86
+ label={`Color mode: ${mode}`}
87
+ onClick={cycle}
88
+ slot="end"
89
+ title={`Color mode: ${mode}`}
90
+ >
91
+ <helix-icon name={MARKS[mode]} />
92
+ </helix-icon-button>
93
+ </helix-app-header>
94
+ )
95
+ }
@@ -1,24 +1,36 @@
1
+ import { Landing } from './landing.js'
2
+ import { Overview } from './overview.js'
3
+ import { usePathname } from './routes.js'
4
+ import { SignupForm } from './signup-form.js'
5
+ import { SiteFooter } from './site-footer.js'
6
+ import { SiteHeader } from './site-header.js'
7
+
8
+ /** What answers each path, in the order the bar offers them. */
9
+ const PAGES = {
10
+ '/': Landing,
11
+ '/join': SignupForm,
12
+ '/overview': Overview,
13
+ }
14
+
1
15
  /**
2
- * Helix components are custom elements, so you write the tags directly. There is
3
- * no wrapper package and nothing to import per component.
16
+ * The shell, and the switch between the three pages.
4
17
  *
5
- * Events are props: `onhelix-before-change` reaches `helix-before-change`. React's
6
- * own bubbling events work as they always do, so `onClick` is fine here.
18
+ * Helix components are custom elements, so you write the tags directly. There is
19
+ * no wrapper package and nothing to import per component: `src/main.tsx`
20
+ * registers them once and every tag in the app is real from then on.
7
21
  */
8
22
  export function App() {
23
+ const path = usePathname()
24
+ const Page = PAGES[path as keyof typeof PAGES] ?? Landing
25
+ /* The bar, the route and the footer, in a column the height of the window.
26
+ `app.css` says why the footer is worth having and why the column is flex. */
9
27
  return (
10
- <helix-flex align="center" direction="column" gap="lg">
11
- <helix-text as="h1">__PROJECT_NAME__</helix-text>
12
- <helix-text>Edit `src/App.tsx` and save.</helix-text>
13
- <helix-button
14
- emphasis="solid"
15
- onClick={() => {
16
- console.log('clicked')
17
- }}
18
- tone="primary"
19
- >
20
- Get started
21
- </helix-button>
22
- </helix-flex>
28
+ <div className="shell">
29
+ <SiteHeader />
30
+ <main className="page">
31
+ <Page />
32
+ </main>
33
+ <SiteFooter />
34
+ </div>
23
35
  )
24
36
  }
@@ -0,0 +1,363 @@
1
+ /*
2
+ * The page around the components.
3
+ *
4
+ * Helix draws its own insides, so what is left to an app stylesheet is the
5
+ * measure, the rhythm between blocks, and the few things a component
6
+ * deliberately leaves open. Everything below is a Helix token or a documented
7
+ * hook, so a theme moves it and both color modes follow without a second copy
8
+ * of anything.
9
+ *
10
+ * **Color appears twice and both places are named.** The wordmark takes
11
+ * `--helix-color-brand-mark`, which is the token for the brand as a graphic
12
+ * rather than as a word, and a chosen row takes `--helix-color-primary-subtle`
13
+ * through the parts the table publishes. Everywhere else this page is ink on the
14
+ * background and the components carry their own.
15
+ */
16
+
17
+ /*
18
+ * **The shell, and what stops a short route ending in a void.** The overview is
19
+ * a header, a table and a caption, which on a 1280 by 900 window left something
20
+ * like four hundred pixels of nothing under it: a page that stopped rather than
21
+ * a page that finished. A column the height of the window with the footer at the
22
+ * end of it puts a floor under every route.
23
+ *
24
+ * **Flex and not grid, because the bar is sticky.** A grid item's containing
25
+ * block is its own grid area, which is one row as tall as the bar, so a sticky
26
+ * header in a grid row sticks for nothing. A flex item's containing block is the
27
+ * flex container, which is the whole document.
28
+ */
29
+ .shell {
30
+ display: flex;
31
+ flex-direction: column;
32
+ min-block-size: 100dvh;
33
+ }
34
+
35
+ /*
36
+ * **56rem, which is a measure rather than a maximum.** A page is as wide as its
37
+ * widest block can be read at, and the widest block here is a four-column table:
38
+ * given the whole of a 1280 window its first column came out 448 pixels across
39
+ * to hold a name, which reads as a table that has been stretched rather than
40
+ * laid out. The bar and the footer are centered in the same width, so the
41
+ * wordmark sits over the first column of the page rather than near it.
42
+ *
43
+ * `flex-grow` is what hands the slack to the page rather than to the footer.
44
+ */
45
+ .page {
46
+ flex-grow: 1;
47
+ margin-inline: auto;
48
+ max-inline-size: 56rem;
49
+ padding-block: var(--helix-space-2xl) var(--helix-space-3xl);
50
+ padding-inline: var(--helix-space-lg);
51
+ }
52
+
53
+ /*
54
+ * Two documented hooks, and both are about agreeing with the page. The bar
55
+ * centers its row in the page's measure, so the wordmark sits over the first
56
+ * column rather than near it. And it draws no line under itself by default,
57
+ * because a header over a page that already has a surface does not need one:
58
+ * this page has no such surface, so it asks for one. A token rather than a
59
+ * color, so it follows the theme.
60
+ */
61
+ helix-app-header {
62
+ --app-header-border-color: var(--helix-color-border);
63
+ --app-header-max-inline-size: 56rem;
64
+ }
65
+
66
+ /* The mark beside the project's name, which is the one graphic on the page and
67
+ the reason `--helix-color-brand-mark` exists: a mark answers to the 3:1 WCAG
68
+ asks of a graphic rather than to the 4.5 a word does, so it may sit closer to
69
+ the brand a theme was seeded with than any text color can. Swap the glyph, or
70
+ replace the whole element with your own logo. */
71
+ .wordmark {
72
+ align-items: center;
73
+ display: inline-flex;
74
+ gap: var(--helix-space-xs);
75
+ }
76
+
77
+ .wordmark helix-icon {
78
+ color: var(--helix-color-brand-mark);
79
+ }
80
+
81
+ /*
82
+ * One rhythm between the blocks of a page, so no page states its own.
83
+ *
84
+ * **`minmax(0, 1fr)` and never a bare column, which is the trap in every grid
85
+ * here.** An implicit track is `auto`, and an `auto` track's minimum is its
86
+ * content's min-content size, so a child that would rather scroll itself widens
87
+ * the track instead and takes the page with it. Measured at 320: the table's
88
+ * columns floor at 344, the column it sits in should have been 288, and
89
+ * `documentElement.scrollWidth` came back 360 against a 320 viewport. The whole
90
+ * page scrolled sideways so that a table with its own scrollport would not have
91
+ * to. Zero as the minimum hands the overflow back to whoever is equipped for it.
92
+ */
93
+ .stack,
94
+ .section,
95
+ .hero,
96
+ .first-screen,
97
+ .fact,
98
+ .facts,
99
+ .notes,
100
+ .split {
101
+ grid-template-columns: minmax(0, 1fr);
102
+ }
103
+
104
+ /* One step between the blocks of a page and a smaller one inside them, which is
105
+ the whole of the rhythm: `3xl` here against the `md` a section puts between
106
+ its heading and its block, so a reader sees three groups rather than six
107
+ evenly spaced things. */
108
+ .stack {
109
+ display: grid;
110
+ gap: var(--helix-space-3xl);
111
+ }
112
+
113
+ /*
114
+ * **The first screen is two things at once**, which is what makes it a screen
115
+ * rather than a stack: what this is, on the left, and the library doing it, on
116
+ * the right. Below 60rem there is no room for two columns that are both worth
117
+ * reading, so they become one and the panel reads as the thing the words were
118
+ * about.
119
+ *
120
+ * `start`, because a grid column stretches to its tallest sibling: the panel
121
+ * beside a taller block of words would otherwise grow to match it and spread its
122
+ * own regions out to fill the difference.
123
+ */
124
+ .first-screen {
125
+ align-items: start;
126
+ display: grid;
127
+ gap: var(--helix-space-xl);
128
+ }
129
+
130
+ @media (width >= 60rem) {
131
+ .first-screen {
132
+ grid-template-columns: minmax(0, 1fr) minmax(0, 20rem);
133
+ }
134
+ }
135
+
136
+ /* A heading and the lines around it are one block, and the gap between them is
137
+ not the gap between blocks. Every route opens with one. */
138
+ .hero {
139
+ display: grid;
140
+ gap: var(--helix-space-xs);
141
+ justify-items: start;
142
+ }
143
+
144
+ /* **A measure for the lede, which the page's own is too wide to be.** A line of
145
+ prose across 56rem is around 110 characters, and the readable range tops out
146
+ near 75. The heading above it is exempt: a heading is scanned rather than
147
+ read along. */
148
+ .lede {
149
+ max-inline-size: 42rem;
150
+ }
151
+
152
+ .actions {
153
+ display: flex;
154
+ flex-wrap: wrap;
155
+ gap: var(--helix-space-sm);
156
+ padding-block-start: var(--helix-space-md);
157
+ }
158
+
159
+ /*
160
+ * **The panel is the library rather than a picture of it**, so the only thing it
161
+ * needs from here is room. Two documented hooks: the figures come down a step
162
+ * from the display size, which is set for a figure on its own and is too loud
163
+ * for three of them in a 20rem column, and the rule between them comes in from
164
+ * `2xl`, which `divided` doubles and which would leave no column to put a
165
+ * number in.
166
+ */
167
+ .specimen {
168
+ --stat-group-gap: var(--helix-space-md);
169
+ --stat-value-size: var(--helix-text-xl);
170
+ }
171
+
172
+ /* A section is its heading and its one block, closer together than two sections
173
+ are. */
174
+ .section {
175
+ display: grid;
176
+ gap: var(--helix-space-md);
177
+ }
178
+
179
+ /*
180
+ * **Three facts about the library, and deliberately not three cards.** A row of
181
+ * identical outlined boxes is the shape a generated page takes, and it is also a
182
+ * claim: a box says each of these is its own object, when they are three
183
+ * paragraphs about the same installed thing. A rule over each column groups them
184
+ * at a fraction of the weight, and leaves the cards below to mean something,
185
+ * which is that they go somewhere.
186
+ *
187
+ * `border-thick` in the foreground rather than a hairline in the border color:
188
+ * this is a heading device rather than a container edge, and it is the one place
189
+ * the page draws in ink.
190
+ */
191
+ .facts {
192
+ display: grid;
193
+ gap: var(--helix-space-lg);
194
+ }
195
+
196
+ @media (width >= 48rem) {
197
+ .facts {
198
+ grid-template-columns: repeat(3, minmax(0, 1fr));
199
+ }
200
+ }
201
+
202
+ .fact {
203
+ align-content: start;
204
+ border-block-start: var(--helix-border-thick) solid var(--helix-color-foreground);
205
+ display: grid;
206
+ gap: var(--helix-space-xs);
207
+ justify-items: start;
208
+ padding-block-start: var(--helix-space-sm);
209
+ }
210
+
211
+ /*
212
+ * **The one departure the grids state.** `helix-grid` takes a fixed count as
213
+ * plain CSS, which is what keeps a server-rendered row of cards correct before
214
+ * any JavaScript arrives. Below this width there is only ever room for one
215
+ * column, and `--grid-columns` is the documented way to say so from outside the
216
+ * shadow root.
217
+ */
218
+ @media (width < 48rem) {
219
+ helix-grid {
220
+ --grid-columns: minmax(0, 1fr);
221
+ }
222
+ }
223
+
224
+ /*
225
+ * **The cards keep `start`, which is `--card-align`'s resting value, and the
226
+ * alternative is worth naming because it looks right and is not.**
227
+ * `space-between` puts the last line of each card on its bottom edge, so a row
228
+ * of cards ends in a straight line. It gets there by sharing the slack out
229
+ * between every region, and the first thing that moves is the body: three cards
230
+ * of six, five and seven lines had their opening lines 50 pixels apart under
231
+ * headings that were level. A reader scans the first line of each, so that is
232
+ * the alignment worth keeping.
233
+ */
234
+
235
+ /* The card fills the link that wraps it, so a row of destinations ends level:
236
+ the anchor is the grid item and stretches, and the card inside it does not.
237
+ The regions stay packed at the top, per `--card-align` above. */
238
+ [data-card] > helix-card {
239
+ block-size: 100%;
240
+ }
241
+
242
+ /* A form is read straight down, and a field wider than about this stops being
243
+ scannable however much room the window has. */
244
+ .form {
245
+ align-content: start;
246
+ display: grid;
247
+ gap: var(--helix-space-lg);
248
+ justify-items: start;
249
+ max-inline-size: 28rem;
250
+ }
251
+
252
+ /* Every control fills the column; the submit is as wide as its own words. */
253
+ .form > :not(helix-button) {
254
+ inline-size: 100%;
255
+ }
256
+
257
+ /*
258
+ * **The form takes a column and the notes take the rest.** A 28rem form on a
259
+ * 1280 window is a third of a page and two thirds of nothing, which is what the
260
+ * measure above cannot fix on its own: the form has a right width and the page
261
+ * does not shrink to it. Narrow, the two stack and the notes read as a
262
+ * postscript, which is the order they are written in.
263
+ */
264
+ .split {
265
+ display: grid;
266
+ gap: var(--helix-space-2xl);
267
+ }
268
+
269
+ @media (width >= 60rem) {
270
+ .split {
271
+ grid-template-columns: minmax(0, 28rem) minmax(0, 1fr);
272
+ }
273
+ }
274
+
275
+ /* `start`, because a grid column stretches to its tallest sibling and a
276
+ stretched grid shares the difference out between its own rows: beside a form
277
+ half a page long, three paragraphs came out 140 pixels apart. */
278
+ .notes {
279
+ align-content: start;
280
+ display: grid;
281
+ gap: var(--helix-space-sm);
282
+ justify-items: start;
283
+ max-inline-size: 32rem;
284
+ }
285
+
286
+ /* The callout takes the column, which `start` above would otherwise size to its
287
+ own words: a notice narrower than the paragraph under it reads as a stray. The
288
+ link beside it is meant to be its own width. */
289
+ .notes > helix-callout {
290
+ inline-size: 100%;
291
+ }
292
+
293
+ /*
294
+ * **The counterpart to the bar, and the reason every route now ends rather than
295
+ * stops.** The rule runs the window while its contents keep the page's measure,
296
+ * which is what a footer under a centered page looks like; the shell above is
297
+ * what holds it to the bottom of a window a route does not fill.
298
+ */
299
+ .site-footer {
300
+ border-block-start: var(--helix-border-thin) solid var(--helix-color-border);
301
+ }
302
+
303
+ .footer-row {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ gap: var(--helix-space-md);
307
+ justify-content: space-between;
308
+ margin-inline: auto;
309
+ max-inline-size: 56rem;
310
+ padding-block: var(--helix-space-lg);
311
+ padding-inline: var(--helix-space-lg);
312
+ }
313
+
314
+ .footer-links {
315
+ display: flex;
316
+ flex-wrap: wrap;
317
+ gap: var(--helix-space-md);
318
+ }
319
+
320
+ /*
321
+ * **Narrow, the gutter gives before the content does.** 24 pixels either side of
322
+ * a 390 window is 48 of 390 spent on margin, and the table needs 355 of what is
323
+ * left. At `md` it has 358 and fits; at `lg` it was 13 pixels short and the last
324
+ * column sat outside the box until the reader scrolled the table sideways.
325
+ */
326
+ @media (width < 40rem) {
327
+ .page {
328
+ padding-block: var(--helix-space-xl) var(--helix-space-2xl);
329
+ padding-inline: var(--helix-space-md);
330
+ }
331
+
332
+ .footer-row {
333
+ padding-inline: var(--helix-space-md);
334
+ }
335
+ }
336
+
337
+ /*
338
+ * **The escape hatch, and the reason a component names its parts.** The table
339
+ * washes a chosen row at the strength `Tag` rests at; this page wants a row
340
+ * somebody has ticked to be findable from across the screen, and there is no
341
+ * token for "more than that". `::part` reaches the boxes the component drew
342
+ * without forking it, and a rule written out here beats the component's own
343
+ * however specific that one is.
344
+ *
345
+ * **Two parts and not one, because a row has no box.** The row is
346
+ * `display: contents` so that one grid can align every column across every row,
347
+ * which leaves the cells as the only thing a selection can be painted on. The
348
+ * table names both: the data cells and the cell holding the checkbox. Naming
349
+ * only the first leaves a pale stripe down the left of every chosen row.
350
+ *
351
+ * The values are still the library's, so a theme carries them like everything
352
+ * else. `primary-subtle` and not `primary-surface`: the second is the solid
353
+ * accent a button stands on and expects white on top of it, so a row painted
354
+ * with it puts the table's own ink on a saturated fill.
355
+ */
356
+ helix-data-table::part(cell-selected),
357
+ helix-data-table::part(select-selected) {
358
+ background-color: var(--helix-color-primary-subtle);
359
+ }
360
+
361
+ helix-data-table::part(cell-selected) {
362
+ font-weight: var(--helix-weight-semibold);
363
+ }
@@ -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,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
+ /** React specifically: properties, events, and the server boundary. */
24
+ react: 'https://fusion.dev/helix/docs/react',
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