@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.
- package/README.md +18 -6
- package/dist/index.js +24 -0
- package/package.json +1 -1
- package/templates/angular/angular.json +57 -0
- package/templates/angular/package.json +28 -0
- package/templates/angular/src/app/app.config.ts +8 -0
- package/templates/angular/src/app/app.html +19 -0
- package/templates/angular/src/app/app.ts +29 -0
- package/templates/angular/src/app/data.ts +81 -0
- package/templates/angular/src/app/docs.ts +29 -0
- package/templates/angular/src/app/landing.html +112 -0
- package/templates/angular/src/app/landing.ts +31 -0
- package/templates/angular/src/app/overview.html +49 -0
- package/templates/angular/src/app/overview.ts +25 -0
- package/templates/angular/src/app/pathname.ts +24 -0
- package/templates/angular/src/app/signup-form.html +91 -0
- package/templates/angular/src/app/signup-form.ts +65 -0
- package/templates/angular/src/app/site-footer.html +10 -0
- package/templates/angular/src/app/site-footer.ts +22 -0
- package/templates/angular/src/app/site-header.html +30 -0
- package/templates/angular/src/app/site-header.ts +58 -0
- package/templates/angular/src/app.css +363 -0
- package/templates/angular/src/diagnostics.ts +24 -0
- package/templates/angular/src/index.html +12 -0
- package/templates/angular/src/main.ts +21 -0
- package/templates/angular/tsconfig.app.json +9 -0
- package/templates/angular/tsconfig.json +34 -0
- package/templates/react-nextjs/app/diagnostics.ts +19 -0
- package/templates/react-nextjs/app/landing.tsx +2 -2
- package/templates/react-nextjs/app/overview.tsx +1 -1
- package/templates/react-nextjs/instrumentation-client.ts +6 -0
- package/templates/react-nextjs/package.json +1 -0
- package/templates/react-nextjs/tsconfig.json +1 -0
- package/templates/react-vite/package.json +1 -0
- package/templates/react-vite/src/diagnostics.ts +24 -0
- package/templates/react-vite/src/main.tsx +6 -0
- package/templates/react-vite/tsconfig.json +1 -0
- package/templates/solid-vite/helix-solid.d.ts +14 -0
- package/templates/solid-vite/index.html +12 -0
- package/templates/solid-vite/package.json +22 -0
- package/templates/solid-vite/src/App.tsx +35 -0
- package/templates/solid-vite/src/app.css +363 -0
- package/templates/solid-vite/src/data.ts +81 -0
- package/templates/solid-vite/src/diagnostics.ts +24 -0
- package/templates/solid-vite/src/docs.ts +29 -0
- package/templates/solid-vite/src/landing.tsx +177 -0
- package/templates/solid-vite/src/main.tsx +23 -0
- package/templates/solid-vite/src/overview.tsx +78 -0
- package/templates/solid-vite/src/routes.ts +22 -0
- package/templates/solid-vite/src/signup-form.tsx +151 -0
- package/templates/solid-vite/src/site-footer.tsx +34 -0
- package/templates/solid-vite/src/site-header.tsx +77 -0
- package/templates/solid-vite/tsconfig.json +27 -0
- package/templates/solid-vite/vite.config.ts +7 -0
- package/templates/svelte-vite/helix-svelte.d.ts +9 -0
- package/templates/svelte-vite/index.html +12 -0
- package/templates/svelte-vite/package.json +23 -0
- package/templates/svelte-vite/src/App.svelte +35 -0
- package/templates/svelte-vite/src/Landing.svelte +137 -0
- package/templates/svelte-vite/src/Overview.svelte +67 -0
- package/templates/svelte-vite/src/SignupForm.svelte +148 -0
- package/templates/svelte-vite/src/SiteFooter.svelte +26 -0
- package/templates/svelte-vite/src/SiteHeader.svelte +72 -0
- package/templates/svelte-vite/src/app.css +363 -0
- package/templates/svelte-vite/src/data.ts +81 -0
- package/templates/svelte-vite/src/diagnostics.ts +24 -0
- package/templates/svelte-vite/src/docs.ts +29 -0
- package/templates/svelte-vite/src/main.ts +23 -0
- package/templates/svelte-vite/src/routes.svelte.ts +19 -0
- package/templates/svelte-vite/tsconfig.json +28 -0
- package/templates/svelte-vite/vite.config.ts +7 -0
- package/templates/vue-vite/helix-vue.d.ts +8 -0
- package/templates/vue-vite/index.html +12 -0
- package/templates/vue-vite/package.json +23 -0
- package/templates/vue-vite/src/App.vue +39 -0
- package/templates/vue-vite/src/LandingPage.vue +139 -0
- package/templates/vue-vite/src/OverviewPage.vue +69 -0
- package/templates/vue-vite/src/SignupForm.vue +149 -0
- package/templates/vue-vite/src/SiteFooter.vue +28 -0
- package/templates/vue-vite/src/SiteHeader.vue +72 -0
- package/templates/vue-vite/src/app.css +363 -0
- package/templates/vue-vite/src/data.ts +81 -0
- package/templates/vue-vite/src/diagnostics.ts +24 -0
- package/templates/vue-vite/src/docs.ts +29 -0
- package/templates/vue-vite/src/main.ts +23 -0
- package/templates/vue-vite/src/routes.ts +26 -0
- package/templates/vue-vite/tsconfig.json +26 -0
- package/templates/vue-vite/vite.config.ts +13 -0
- package/templates.json +41 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
<header class="hero">
|
|
2
|
+
<helix-text emphasis="muted" variant="label">Form</helix-text>
|
|
3
|
+
<helix-text as="h1" variant="heading">Join the roster</helix-text>
|
|
4
|
+
<helix-text as="p" class="lede" emphasis="muted" variant="body">
|
|
5
|
+
Five controls, and a <helix-code><form></helix-code> reads every one of them the way
|
|
6
|
+
it reads an <helix-code>input</helix-code>.
|
|
7
|
+
</helix-text>
|
|
8
|
+
</header>
|
|
9
|
+
|
|
10
|
+
<!-- The form takes a column of its own and the notes take what is left. At
|
|
11
|
+
390 the two stack, and the notes read as a postscript. -->
|
|
12
|
+
<div class="split">
|
|
13
|
+
<form class="form" (submit)="join($event)">
|
|
14
|
+
<helix-input autocomplete="name" label="Name" name="name" required></helix-input>
|
|
15
|
+
|
|
16
|
+
<!-- Validation is the platform's: `type` and `required` are set on the
|
|
17
|
+
real input inside, and its own message shows once the reader has
|
|
18
|
+
touched the field. Set `feedback` to say it in your own words. -->
|
|
19
|
+
<helix-input
|
|
20
|
+
hint="Where we send the welcome note."
|
|
21
|
+
label="Email"
|
|
22
|
+
name="email"
|
|
23
|
+
required
|
|
24
|
+
type="email"
|
|
25
|
+
></helix-input>
|
|
26
|
+
|
|
27
|
+
<!-- `(helix-before-change)` binds with `addEventListener`, so it takes
|
|
28
|
+
the event's own name. -->
|
|
29
|
+
<helix-input
|
|
30
|
+
hint="Lowercase and hyphens, whatever you type."
|
|
31
|
+
label="Handle"
|
|
32
|
+
name="handle"
|
|
33
|
+
[value]="handle()"
|
|
34
|
+
(helix-before-change)="takeHandle($event)"
|
|
35
|
+
></helix-input>
|
|
36
|
+
|
|
37
|
+
<helix-select label="Team" name="team" placeholder="Pick one" required>
|
|
38
|
+
@for (team of teams; track team) {
|
|
39
|
+
<helix-option [value]="team">{{ team }}</helix-option>
|
|
40
|
+
}
|
|
41
|
+
</helix-select>
|
|
42
|
+
|
|
43
|
+
<!-- A switch is a thing happening now, which is why it says on and off.
|
|
44
|
+
It still submits, as `on`. -->
|
|
45
|
+
<helix-switch label="Send me the weekly digest" name="digest"></helix-switch>
|
|
46
|
+
|
|
47
|
+
<helix-button emphasis="solid" tone="primary" type="submit">Join</helix-button>
|
|
48
|
+
</form>
|
|
49
|
+
|
|
50
|
+
<aside class="notes">
|
|
51
|
+
<!-- The one fact you need before typing your name into a page you have
|
|
52
|
+
had for five minutes, so it is a callout. `tone` is the meaning,
|
|
53
|
+
and the component draws it with a tint and a bar on the inline
|
|
54
|
+
start, so it survives being read in grayscale. -->
|
|
55
|
+
<helix-callout tone="info">
|
|
56
|
+
Nothing is posted anywhere. Submit it and the message shows exactly what a server would
|
|
57
|
+
have been handed.
|
|
58
|
+
</helix-callout>
|
|
59
|
+
<helix-text as="h2" variant="title">What this is showing</helix-text>
|
|
60
|
+
<helix-text as="p" emphasis="muted" variant="body">
|
|
61
|
+
Every control is a form-associated custom element, so the submit reads them with
|
|
62
|
+
<helix-code>new FormData(form)</helix-code> and nothing gathers the answers by hand.
|
|
63
|
+
Reset the form and they all go back, the way an <helix-code>input</helix-code> does.
|
|
64
|
+
</helix-text>
|
|
65
|
+
<helix-text as="p" emphasis="muted" variant="body">
|
|
66
|
+
Leave the email wrong and the complaint is the browser's own, in the reader's language.
|
|
67
|
+
Handle is the controlled one: it refuses the change and writes back what it will accept.
|
|
68
|
+
</helix-text>
|
|
69
|
+
<a data-link [href]="docs.forms">Forms</a>
|
|
70
|
+
</aside>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<!-- **The region is rendered whether or not there is anything in it.**
|
|
74
|
+
Assistive technology subscribes to a live region, and one that arrives
|
|
75
|
+
already holding its message is often never announced. A dismissed
|
|
76
|
+
message asks to go through `helix-dismiss`. -->
|
|
77
|
+
<helix-toast>
|
|
78
|
+
@for (message of messages(); track message.id) {
|
|
79
|
+
<helix-toast-item
|
|
80
|
+
dismissible
|
|
81
|
+
[duration]="8000"
|
|
82
|
+
tone="success"
|
|
83
|
+
(helix-dismiss)="dismiss(message.id)"
|
|
84
|
+
>
|
|
85
|
+
<helix-flex direction="column" gap="2xs">
|
|
86
|
+
<helix-text variant="body-strong">Read by the form</helix-text>
|
|
87
|
+
<helix-text variant="code">{{ message.text }}</helix-text>
|
|
88
|
+
</helix-flex>
|
|
89
|
+
</helix-toast-item>
|
|
90
|
+
}
|
|
91
|
+
</helix-toast>
|
|
@@ -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 in here, with its API, its parts and its examples, has a page.
|
|
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,58 @@
|
|
|
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, so the control draws the same glyph before
|
|
35
|
+
and after the reader first touches it. */
|
|
36
|
+
protected readonly mode = signal<Mode>('system')
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One attribute, two modes, and no second stylesheet.
|
|
40
|
+
*
|
|
41
|
+
* Every Helix token is written once as a `light-dark()` pair, so
|
|
42
|
+
* `color-scheme` decides which half resolves. `data-color-mode` sets it, and
|
|
43
|
+
* since `color-scheme` inherits, the attribute works on any element: put it
|
|
44
|
+
* on a panel for a light preview inside a dark page.
|
|
45
|
+
*
|
|
46
|
+
* `system` removes the attribute. Writing out whichever mode is current would
|
|
47
|
+
* freeze the page there the moment the reader's machine changed.
|
|
48
|
+
*/
|
|
49
|
+
protected cycle(): void {
|
|
50
|
+
const next = MODES[(MODES.indexOf(this.mode()) + 1) % MODES.length] ?? 'system'
|
|
51
|
+
this.mode.set(next)
|
|
52
|
+
if (next === 'system') {
|
|
53
|
+
delete document.documentElement.dataset['colorMode']
|
|
54
|
+
return
|
|
55
|
+
}
|
|
56
|
+
document.documentElement.dataset['colorMode'] = next
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -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,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>
|