@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,65 @@
1
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, signal } from '@angular/core'
2
+ import { teams } from './data'
3
+ import { DOCS } from './docs'
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. No `FormsModule` is involved.
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
+ @Component({
24
+ host: { class: 'stack' },
25
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
26
+ selector: 'div[app-signup-form]',
27
+ templateUrl: './signup-form.html',
28
+ })
29
+ export class SignupForm {
30
+ protected readonly docs = DOCS
31
+ protected readonly teams = teams
32
+ protected readonly handle = signal('')
33
+ protected readonly messages = signal<readonly Message[]>([])
34
+
35
+ /**
36
+ * The page takes the submit, so nothing navigates. Remove `preventDefault()`
37
+ * and the form posts to the server like any other, with the same names.
38
+ */
39
+ protected join(event: SubmitEvent): void {
40
+ event.preventDefault()
41
+ const answers = [...new FormData(event.currentTarget as HTMLFormElement).entries()]
42
+ .map(([field, value]) => `${field}=${value}`)
43
+ .join(' ')
44
+ this.messages.update((sent) => [...sent, { id: Date.now(), text: answers }])
45
+ }
46
+
47
+ /**
48
+ * **A controlled value, which is the one habit to unlearn.** A Helix element
49
+ * is not read-only: writing `value` seeds it, and the element goes on
50
+ * applying its own changes. So taking the decision means refusing the change
51
+ * with `preventDefault()`, then setting the value you want. Leaving `handle`
52
+ * as it was does not refuse anything.
53
+ *
54
+ * No typings ship for Angular, so the event's detail is named here.
55
+ */
56
+ protected takeHandle(event: Event): void {
57
+ event.preventDefault()
58
+ const { value } = (event as CustomEvent<{ value: string }>).detail
59
+ this.handle.set(value.toLowerCase().replaceAll(/[^a-z0-9-]/g, ''))
60
+ }
61
+
62
+ protected dismiss(id: number): void {
63
+ this.messages.update((sent) => sent.filter((one) => one.id !== id))
64
+ }
65
+ }
@@ -0,0 +1,10 @@
1
+ <div class="footer-row">
2
+ <helix-text as="p" emphasis="subtle" variant="caption">
3
+ Every component here has a page, with its API, its parts, and its examples.
4
+ </helix-text>
5
+ <div class="footer-links">
6
+ <a data-link="subtle" [href]="docs.gallery">Components</a>
7
+ <a data-link="subtle" [href]="docs.elements">Documentation</a>
8
+ <a data-link="subtle" [href]="docs.angular">Angular</a>
9
+ </div>
10
+ </div>
@@ -0,0 +1,22 @@
1
+ import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
2
+ import { DOCS } from './docs'
3
+
4
+ /**
5
+ * The bar's counterpart, on every route.
6
+ *
7
+ * **It puts a floor under the content.** The overview is a heading, a table,
8
+ * and a caption, which on a tall window left the bottom third of the screen
9
+ * empty. The footer is also where the links you want once belong.
10
+ *
11
+ * `data-link="subtle"` reads as body text until it is hovered, which suits a
12
+ * row of links under a rule.
13
+ */
14
+ @Component({
15
+ host: { class: 'site-footer' },
16
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
17
+ selector: 'footer[app-site-footer]',
18
+ templateUrl: './site-footer.html',
19
+ })
20
+ export class SiteFooter {
21
+ protected readonly docs = DOCS
22
+ }
@@ -0,0 +1,30 @@
1
+ <helix-app-header material="glass" sticky>
2
+ <!-- The wordmark is the way back to the first page, so there is no third
3
+ destination in the bar saying the same thing. It is also the only name
4
+ of the project on the screen. The mark is a placeholder for your own,
5
+ drawn in `--helix-color-brand-mark`, the token for the brand as a
6
+ graphic. -->
7
+ <a class="wordmark" data-link="quiet" href="#/" slot="start">
8
+ <helix-icon name="cube"></helix-icon>
9
+ <helix-text variant="body-strong">__PROJECT_NAME__</helix-text>
10
+ </a>
11
+
12
+ <!-- The links are yours. Navigation owns the marker that travels between
13
+ them and renders no anchors of its own. The base layer styles them,
14
+ `aria-current="page"` included. -->
15
+ <helix-navigation label="Pages">
16
+ <a [attr.aria-current]="path() === '/overview' ? 'page' : null" href="#/overview">Overview</a>
17
+ <a [attr.aria-current]="path() === '/join' ? 'page' : null" href="#/join">Join</a>
18
+ </helix-navigation>
19
+
20
+ <!-- An icon with no words has no accessible name until it is given one,
21
+ which is why this is its own element. -->
22
+ <helix-icon-button
23
+ [label]="'Color mode: ' + mode()"
24
+ slot="end"
25
+ [title]="'Color mode: ' + mode()"
26
+ (click)="cycle()"
27
+ >
28
+ <helix-icon [name]="marks[mode()]"></helix-icon>
29
+ </helix-icon-button>
30
+ </helix-app-header>
@@ -0,0 +1,57 @@
1
+ import { Component, CUSTOM_ELEMENTS_SCHEMA, inject, signal } from '@angular/core'
2
+ import { Pathname } from './pathname'
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
+ * `CUSTOM_ELEMENTS_SCHEMA` is what lets Angular compile a template holding
16
+ * tags it does not know. It turns off the unknown-element check for this
17
+ * template alone, so keep it on the components that render Helix.
18
+ *
19
+ * **A tag of its own, drawn as no box.** Selected by an attribute on
20
+ * `helix-app-header`, the component would leave that attribute on a Helix
21
+ * element, which Helix diagnostics reports as one the element does not take.
22
+ * `display: contents` makes the bar a child of the shell's column again, which
23
+ * a sticky header needs: it sticks only within its parent's box.
24
+ */
25
+ @Component({
26
+ host: { style: 'display: contents' },
27
+ schemas: [CUSTOM_ELEMENTS_SCHEMA],
28
+ selector: 'app-site-header',
29
+ templateUrl: './site-header.html',
30
+ })
31
+ export class SiteHeader {
32
+ protected readonly path = inject(Pathname).current
33
+ protected readonly marks = MARKS
34
+ /* `system` is the resting state, the mode the page follows until the control is pressed. */
35
+ protected readonly mode = signal<Mode>('system')
36
+
37
+ /**
38
+ * One attribute, two modes, and no second stylesheet.
39
+ *
40
+ * Every Helix token is written once as a `light-dark()` pair, so
41
+ * `color-scheme` decides which half resolves. `data-color-mode` sets it, and
42
+ * since `color-scheme` inherits, the attribute works on any element: put it
43
+ * on a panel for a light preview inside a dark page.
44
+ *
45
+ * `system` removes the attribute. Writing out whichever mode is current would
46
+ * hold the page there when the system's own setting changed.
47
+ */
48
+ protected cycle(): void {
49
+ const next = MODES[(MODES.indexOf(this.mode()) + 1) % MODES.length] ?? 'system'
50
+ this.mode.set(next)
51
+ if (next === 'system') {
52
+ delete document.documentElement.dataset['colorMode']
53
+ return
54
+ }
55
+ document.documentElement.dataset['colorMode'] = next
56
+ }
57
+ }
@@ -0,0 +1,355 @@
1
+ /*
2
+ * The page around the components.
3
+ *
4
+ * Helix draws the inside of each component, so an app stylesheet holds the
5
+ * measure, the rhythm between blocks, and the few decisions a component leaves
6
+ * open. Everything below is a Helix token or a documented hook, so a theme
7
+ * changes it, and both color modes follow from one copy.
8
+ *
9
+ * **Color appears in two places, and both are named.** The wordmark takes
10
+ * `--helix-color-brand-mark`, the token for the brand as a graphic, and a
11
+ * chosen row takes `--helix-color-primary-subtle` through the parts the table
12
+ * publishes. Everywhere else the page is ink on the background, and the
13
+ * components carry their own color.
14
+ */
15
+
16
+ /*
17
+ * **The shell: a column the height of the window, with the footer at its end.**
18
+ * The overview is a header, a table, and a caption, which on a 1280 by 900
19
+ * window left about four hundred empty pixels under it. The column puts a floor
20
+ * under every route.
21
+ *
22
+ * **A flex column, because the bar is sticky.** A grid item's containing block
23
+ * is its own grid area, one row as tall as the bar, so a sticky header in a
24
+ * grid row has nowhere to stick. A flex item's containing block is the flex
25
+ * container, which here is the whole document.
26
+ */
27
+ .shell {
28
+ display: flex;
29
+ flex-direction: column;
30
+ min-block-size: 100dvh;
31
+ }
32
+
33
+ /*
34
+ * **56rem, the width the widest block reads well at.** The widest block here is
35
+ * a four-column table: given the whole of a 1280 window, its first column came
36
+ * out 448 pixels across to hold a name, which reads as a stretched table. The
37
+ * bar and the footer are centered in the same width, so the wordmark sits over
38
+ * the first column of the page.
39
+ *
40
+ * `flex-grow` gives the spare height to the page, and the footer keeps its own.
41
+ */
42
+ .page {
43
+ flex-grow: 1;
44
+ margin-inline: auto;
45
+ max-inline-size: 56rem;
46
+ padding-block: var(--helix-space-2xl) var(--helix-space-3xl);
47
+ padding-inline: var(--helix-space-lg);
48
+ }
49
+
50
+ /*
51
+ * Two documented hooks, both to match the page. The bar centers its row in the
52
+ * page's measure, so the wordmark sits over the first column. And the bar draws
53
+ * no line under itself by default, since a header over a page with its own
54
+ * surface needs none: this page has no such surface, so it sets one. The line
55
+ * is a token, so it follows the theme.
56
+ */
57
+ helix-app-header {
58
+ --app-header-border-color: var(--helix-color-border);
59
+ --app-header-max-inline-size: 56rem;
60
+ }
61
+
62
+ /* The mark beside the project's name, the one graphic on the page and the
63
+ reason `--helix-color-brand-mark` exists. WCAG asks 3:1 of a graphic and 4.5:1
64
+ of text, so a mark can sit closer to a theme's brand color than any text
65
+ color can. Swap the glyph, or replace the whole element with your own logo. */
66
+ .wordmark {
67
+ align-items: center;
68
+ display: inline-flex;
69
+ gap: var(--helix-space-xs);
70
+ }
71
+
72
+ .wordmark helix-icon {
73
+ color: var(--helix-color-brand-mark);
74
+ }
75
+
76
+ /*
77
+ * One rhythm between the blocks of a page, so no page sets its own.
78
+ *
79
+ * **`minmax(0, 1fr)` for every column, the guard every grid here needs.** An
80
+ * implicit track is `auto`, and an `auto` track's minimum is its content's
81
+ * min-content size, so a child that scrolls itself widens the track and takes
82
+ * the page with it. Measured at 320: the table's columns floor at 344, the
83
+ * column it sits in should have been 288, and `documentElement.scrollWidth`
84
+ * came back 360 against a 320 viewport, so the whole page scrolled sideways. A
85
+ * minimum of zero leaves the overflow to the table, which scrolls its own box.
86
+ */
87
+ .stack,
88
+ .section,
89
+ .hero,
90
+ .first-screen,
91
+ .features,
92
+ .notes,
93
+ .split {
94
+ grid-template-columns: minmax(0, 1fr);
95
+ }
96
+
97
+ /* One step between the blocks of a page and a smaller one inside them, which is
98
+ the whole of the rhythm: `3xl` here, and the `md` a section puts between its
99
+ heading and its block. */
100
+ .stack {
101
+ display: grid;
102
+ gap: var(--helix-space-3xl);
103
+ }
104
+
105
+ /*
106
+ * **The first screen: one lit panel, with what the starter is on the left and
107
+ * the library at work on the right.** Below 60rem two readable columns do not
108
+ * fit, so they become one, and the glass card follows the words.
109
+ *
110
+ * `isolation` makes the panel its own stacking context, so the light behind it
111
+ * sits under the content and above the page, and `overflow: clip` keeps the
112
+ * light inside the rounded corners.
113
+ */
114
+ .first-screen {
115
+ align-items: center;
116
+ border: var(--helix-border-thin) solid var(--helix-color-border);
117
+ border-radius: var(--helix-radii-lg);
118
+ display: grid;
119
+ gap: var(--helix-space-2xl);
120
+ isolation: isolate;
121
+ overflow: clip;
122
+ padding: var(--helix-space-3xl) var(--helix-space-2xl);
123
+ position: relative;
124
+ }
125
+
126
+ @media (width >= 60rem) {
127
+ .first-screen {
128
+ grid-template-columns: minmax(0, 1fr) minmax(0, 18rem);
129
+ }
130
+ }
131
+
132
+ /* The light, under everything in the panel and out of the way of the pointer,
133
+ and faded out behind the words so they keep their contrast: it is at full
134
+ strength only behind the card, which is below the words in one column and
135
+ beside them in two. */
136
+ .first-light {
137
+ block-size: auto;
138
+ inset: 0;
139
+ mask-image: linear-gradient(to bottom, transparent 40%, black 80%);
140
+ pointer-events: none;
141
+ position: absolute;
142
+ z-index: var(--helix-layer-beneath);
143
+ }
144
+
145
+ @media (width >= 60rem) {
146
+ .first-light {
147
+ mask-image: linear-gradient(to right, transparent 35%, black 70%);
148
+ }
149
+ }
150
+
151
+ /* A heading and the lines around it are one block, with a smaller gap than the
152
+ one between blocks. Every route opens with one. */
153
+ .hero {
154
+ display: grid;
155
+ gap: var(--helix-space-xs);
156
+ justify-items: start;
157
+ }
158
+
159
+ /* The first screen's heading takes more room around it than a route's, and
160
+ its lede a step more size, wrapped to even lines. */
161
+ .first-screen .hero {
162
+ gap: var(--helix-space-md);
163
+ }
164
+
165
+ .first-screen .lede {
166
+ font-size: var(--helix-text-lg);
167
+ text-wrap: balance;
168
+ }
169
+
170
+ /* **A measure for the lede, narrower than the page's.** A line of prose across
171
+ 56rem is around 110 characters, and the readable range tops out near 75. The
172
+ heading above it keeps the page's measure, since a heading is scanned. */
173
+ .lede {
174
+ max-inline-size: 42rem;
175
+ }
176
+
177
+ .actions {
178
+ display: flex;
179
+ flex-wrap: wrap;
180
+ gap: var(--helix-space-sm);
181
+ padding-block-start: var(--helix-space-md);
182
+ }
183
+
184
+ /*
185
+ * **The panel is the library at work**, so all it needs from here is room. Two
186
+ * documented hooks: the figures come down a step from the display size, which
187
+ * suits one figure on its own and is too loud for three in an 18rem column, and
188
+ * the gap around the rule between them comes in from `2xl`, which `divided`
189
+ * doubles, leaving no room for a number.
190
+ */
191
+ .specimen {
192
+ --stat-group-gap: var(--helix-space-md);
193
+ --stat-value-size: var(--helix-text-xl);
194
+ }
195
+
196
+ /* A section is its heading and its one block, closer together than two sections
197
+ are. */
198
+ .section {
199
+ display: grid;
200
+ gap: var(--helix-space-md);
201
+ }
202
+
203
+ /* Three tiles, side by side where there is room for three. */
204
+ .features {
205
+ display: grid;
206
+ gap: var(--helix-space-lg);
207
+ }
208
+
209
+ @media (width >= 48rem) {
210
+ .features {
211
+ grid-template-columns: repeat(3, minmax(0, 1fr));
212
+ }
213
+ }
214
+
215
+ /* The card fills the link that wraps it, so the row of tiles ends level: the
216
+ anchor is the grid item, which stretches, and the card takes its full height. */
217
+ [data-card] > helix-card {
218
+ block-size: 100%;
219
+ }
220
+
221
+ /* Each tile's icon, on a tint of the brand. */
222
+ .feature-mark {
223
+ --icon-size: 1.25rem;
224
+ background-color: var(--helix-color-primary-subtle);
225
+ border-radius: var(--helix-radii-md);
226
+ color: var(--helix-color-primary);
227
+ display: inline-flex;
228
+ inline-size: fit-content;
229
+ padding: var(--helix-space-xs);
230
+ }
231
+
232
+ /* A form is read straight down, and a field wider than about 28rem is hard to
233
+ scan, however much room the window has. */
234
+ .form {
235
+ align-content: start;
236
+ display: grid;
237
+ gap: var(--helix-space-lg);
238
+ justify-items: start;
239
+ max-inline-size: 28rem;
240
+ }
241
+
242
+ /* Every control fills the column; the submit is as wide as its own words. */
243
+ .form > :not(helix-button) {
244
+ inline-size: 100%;
245
+ }
246
+
247
+ /*
248
+ * **The form takes a column and the notes take the rest.** A 28rem form alone
249
+ * on a 1280 window fills a third of the page and leaves two thirds empty: the
250
+ * form has a right width, and the page keeps its own. Narrow, the two stack,
251
+ * and the notes read as a postscript, the order they are written in.
252
+ */
253
+ .split {
254
+ display: grid;
255
+ gap: var(--helix-space-2xl);
256
+ }
257
+
258
+ @media (width >= 60rem) {
259
+ .split {
260
+ grid-template-columns: minmax(0, 28rem) minmax(0, 1fr);
261
+ }
262
+ }
263
+
264
+ /* `start`, because a grid column stretches to its tallest sibling and a
265
+ stretched grid shares the difference out between its own rows: beside a form
266
+ half a page long, three paragraphs came out 140 pixels apart. */
267
+ .notes {
268
+ align-content: start;
269
+ display: grid;
270
+ gap: var(--helix-space-sm);
271
+ justify-items: start;
272
+ max-inline-size: 32rem;
273
+ }
274
+
275
+ /* The callout takes the column, which `start` above would otherwise size to its
276
+ own words: a notice narrower than the paragraph under it reads as a stray. The
277
+ link beside it is meant to be its own width. */
278
+ .notes > helix-callout {
279
+ inline-size: 100%;
280
+ }
281
+
282
+ /*
283
+ * **The counterpart to the bar, which gives every route an end.** The rule runs
284
+ * the width of the window while its contents keep the page's measure, as a
285
+ * footer under a centered page does. The shell above holds it to the bottom of a
286
+ * window a route does not fill.
287
+ */
288
+ .site-footer {
289
+ border-block-start: var(--helix-border-thin) solid var(--helix-color-border);
290
+ }
291
+
292
+ .footer-row {
293
+ display: flex;
294
+ flex-wrap: wrap;
295
+ gap: var(--helix-space-md);
296
+ justify-content: space-between;
297
+ margin-inline: auto;
298
+ max-inline-size: 56rem;
299
+ padding-block: var(--helix-space-lg);
300
+ padding-inline: var(--helix-space-lg);
301
+ }
302
+
303
+ .footer-links {
304
+ display: flex;
305
+ flex-wrap: wrap;
306
+ gap: var(--helix-space-md);
307
+ }
308
+
309
+ /*
310
+ * **Narrow, the gutter gives first.** 24 pixels either side of a 390 window
311
+ * spend 48 of 390 on margin, and the table needs 355 of what is left. At `md` it
312
+ * has 358 and fits; at `lg` it was 13 pixels short, and the last column sat
313
+ * outside the box until you scrolled the table sideways.
314
+ */
315
+ @media (width < 40rem) {
316
+ .first-screen {
317
+ padding: var(--helix-space-2xl) var(--helix-space-lg);
318
+ }
319
+
320
+ .page {
321
+ padding-block: var(--helix-space-xl) var(--helix-space-2xl);
322
+ padding-inline: var(--helix-space-md);
323
+ }
324
+
325
+ .footer-row {
326
+ padding-inline: var(--helix-space-md);
327
+ }
328
+ }
329
+
330
+ /*
331
+ * **The escape hatch, and the reason a component names its parts.** The table
332
+ * tints a chosen row at the strength `Tag` rests at, and this page wants a
333
+ * ticked row easy to find from across the screen, a step past any token.
334
+ * `::part` reaches the boxes the component drew, with no fork, and a rule
335
+ * written out here beats the component's own, however specific that one is.
336
+ *
337
+ * **Two parts, because a row has no box.** The row is `display: contents`, so
338
+ * one grid can align every column across every row, and the cells are what a
339
+ * selection is painted on. The table names both: the data cells and the cell
340
+ * holding the checkbox. Naming only the first leaves a pale stripe down the
341
+ * left of every chosen row.
342
+ *
343
+ * The values are still the library's, so a theme carries them like everything
344
+ * else. `primary-subtle`, because `primary-surface` is the solid accent a
345
+ * button stands on, made for white text, so a row painted with it would put the
346
+ * table's own ink on a saturated fill.
347
+ */
348
+ helix-data-table::part(cell-selected),
349
+ helix-data-table::part(select-selected) {
350
+ background-color: var(--helix-color-primary-subtle);
351
+ }
352
+
353
+ helix-data-table::part(cell-selected) {
354
+ font-weight: var(--helix-weight-semibold);
355
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Helix diagnostics, which `main.ts` 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,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
+ <base href="/" />
8
+ </head>
9
+ <body>
10
+ <app-root></app-root>
11
+ </body>
12
+ </html>
@@ -0,0 +1,21 @@
1
+ import { isDevMode } from '@angular/core'
2
+ import { bootstrapApplication } from '@angular/platform-browser'
3
+
4
+ /* A custom element registers as a side effect of being imported, so this line
5
+ makes every Helix tag in the app real. Import the whole library while you are
6
+ finding your feet. `@fusion.dev/helix/button` and its siblings are subpath
7
+ exports, for when you want to pay for only what you use. The stylesheets are
8
+ in `angular.json`, since Angular takes global styles there. */
9
+ import '@fusion.dev/helix'
10
+
11
+ import { App } from './app/app'
12
+ import { appConfig } from './app/app.config'
13
+
14
+ /* **Diagnostics in development only.** `ng serve` runs in development, and
15
+ `ng build` in production, where `isDevMode()` is false and the chunk this
16
+ imports is never requested. */
17
+ if (isDevMode()) {
18
+ void import('./diagnostics')
19
+ }
20
+
21
+ bootstrapApplication(App, appConfig).catch((error: unknown) => console.error(error))
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": []
5
+ },
6
+ "include": [
7
+ "src/**/*.ts"
8
+ ]
9
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "compileOnSave": false,
3
+ "compilerOptions": {
4
+ "experimentalDecorators": true,
5
+ "importHelpers": true,
6
+ "isolatedModules": true,
7
+ "lib": [
8
+ "DOM",
9
+ "DOM.Iterable",
10
+ "ES2023"
11
+ ],
12
+ "module": "preserve",
13
+ "noFallthroughCasesInSwitch": true,
14
+ "noImplicitOverride": true,
15
+ "noImplicitReturns": true,
16
+ "noPropertyAccessFromIndexSignature": true,
17
+ "resolveJsonModule": true,
18
+ "skipLibCheck": true,
19
+ "strict": true,
20
+ "target": "ES2022"
21
+ },
22
+ "angularCompilerOptions": {
23
+ "enableI18nLegacyMessageIdFormat": false,
24
+ "strictInjectionParameters": true,
25
+ "strictInputAccessModifiers": true,
26
+ "strictTemplates": true
27
+ },
28
+ "files": [],
29
+ "references": [
30
+ {
31
+ "path": "./tsconfig.app.json"
32
+ }
33
+ ]
34
+ }