@mystravil/hardpoint 0.0.0-stage → 0.1.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 (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +346 -0
  5. package/dist/components/AppShell.svelte +698 -0
  6. package/dist/components/AppShell.svelte.d.ts +77 -0
  7. package/dist/components/Button.svelte +174 -0
  8. package/dist/components/Button.svelte.d.ts +20 -0
  9. package/dist/components/Checkbox.svelte +198 -0
  10. package/dist/components/Checkbox.svelte.d.ts +16 -0
  11. package/dist/components/CheckboxGroup.svelte +99 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
  13. package/dist/components/Combobox.svelte +824 -0
  14. package/dist/components/Combobox.svelte.d.ts +63 -0
  15. package/dist/components/Dialog.svelte +356 -0
  16. package/dist/components/Dialog.svelte.d.ts +43 -0
  17. package/dist/components/Disclosure.svelte +201 -0
  18. package/dist/components/Disclosure.svelte.d.ts +27 -0
  19. package/dist/components/EmptyState.svelte +88 -0
  20. package/dist/components/EmptyState.svelte.d.ts +22 -0
  21. package/dist/components/ErrorState.svelte +135 -0
  22. package/dist/components/ErrorState.svelte.d.ts +26 -0
  23. package/dist/components/KeyValueList.svelte +114 -0
  24. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  25. package/dist/components/LoadRegion.svelte +219 -0
  26. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  27. package/dist/components/MediaFrame.svelte +110 -0
  28. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  29. package/dist/components/MediaImage.svelte +256 -0
  30. package/dist/components/MediaImage.svelte.d.ts +16 -0
  31. package/dist/components/Menu.svelte +258 -0
  32. package/dist/components/Menu.svelte.d.ts +36 -0
  33. package/dist/components/MenuCheckboxItem.svelte +119 -0
  34. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  35. package/dist/components/MenuGroup.svelte +50 -0
  36. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  37. package/dist/components/MenuItem.svelte +94 -0
  38. package/dist/components/MenuItem.svelte.d.ts +19 -0
  39. package/dist/components/MenuRadioGroup.svelte +62 -0
  40. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  41. package/dist/components/MenuRadioItem.svelte +110 -0
  42. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  43. package/dist/components/MenuSeparator.svelte +28 -0
  44. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  45. package/dist/components/NativeSelect.svelte +119 -0
  46. package/dist/components/NativeSelect.svelte.d.ts +46 -0
  47. package/dist/components/Pagination.svelte +132 -0
  48. package/dist/components/Pagination.svelte.d.ts +18 -0
  49. package/dist/components/Popover.svelte +222 -0
  50. package/dist/components/Popover.svelte.d.ts +35 -0
  51. package/dist/components/Progress.svelte +157 -0
  52. package/dist/components/Progress.svelte.d.ts +34 -0
  53. package/dist/components/Select.svelte +405 -0
  54. package/dist/components/Select.svelte.d.ts +42 -0
  55. package/dist/components/Skeleton.svelte +149 -0
  56. package/dist/components/Skeleton.svelte.d.ts +24 -0
  57. package/dist/components/Spinner.svelte +117 -0
  58. package/dist/components/Spinner.svelte.d.ts +13 -0
  59. package/dist/components/Stat.svelte +253 -0
  60. package/dist/components/Stat.svelte.d.ts +46 -0
  61. package/dist/components/Steps.svelte +189 -0
  62. package/dist/components/Steps.svelte.d.ts +31 -0
  63. package/dist/components/Switch.svelte +222 -0
  64. package/dist/components/Switch.svelte.d.ts +18 -0
  65. package/dist/components/Table.svelte +504 -0
  66. package/dist/components/Table.svelte.d.ts +92 -0
  67. package/dist/components/Tabs.svelte +246 -0
  68. package/dist/components/Tabs.svelte.d.ts +44 -0
  69. package/dist/components/Tag.svelte +40 -0
  70. package/dist/components/Tag.svelte.d.ts +15 -0
  71. package/dist/components/TextField.svelte +256 -0
  72. package/dist/components/TextField.svelte.d.ts +32 -0
  73. package/dist/components/Timeline.svelte +156 -0
  74. package/dist/components/Timeline.svelte.d.ts +30 -0
  75. package/dist/components/Toaster.svelte +411 -0
  76. package/dist/components/Toaster.svelte.d.ts +11 -0
  77. package/dist/components/Tooltip.svelte +200 -0
  78. package/dist/components/Tooltip.svelte.d.ts +38 -0
  79. package/dist/components/TooltipGroup.svelte +46 -0
  80. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  81. package/dist/components/internal/Scrim.svelte +52 -0
  82. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  83. package/dist/components/internal/Sheet.svelte +263 -0
  84. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  85. package/dist/components/internal/fade-in.d.ts +10 -0
  86. package/dist/components/internal/fade-in.js +20 -0
  87. package/dist/components/internal/resolve.d.ts +8 -0
  88. package/dist/components/internal/resolve.js +247 -0
  89. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  90. package/dist/components/internal/swipe-dismiss.js +220 -0
  91. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  92. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  93. package/dist/components/toast.svelte.d.ts +57 -0
  94. package/dist/components/toast.svelte.js +84 -0
  95. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  96. package/dist/fonts/OFL.txt +93 -0
  97. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  98. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  99. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  100. package/dist/fonts.css +71 -0
  101. package/dist/index.d.ts +39 -0
  102. package/dist/index.js +39 -0
  103. package/dist/motion/Motion.svelte +15 -0
  104. package/dist/motion/Motion.svelte.d.ts +9 -0
  105. package/dist/motion/context.d.ts +18 -0
  106. package/dist/motion/context.js +26 -0
  107. package/dist/motion/index.d.ts +3 -0
  108. package/dist/motion/index.js +3 -0
  109. package/dist/motion/transitions.d.ts +31 -0
  110. package/dist/motion/transitions.js +112 -0
  111. package/dist/tokens.css +279 -0
  112. package/package.json +76 -6
@@ -0,0 +1,117 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+
4
+ export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
5
+ /**
6
+ * What the app is waiting for, such as `Loading projects`. Shown beside
7
+ * the glyph and announced politely once the spinner appears.
8
+ */
9
+ label: string;
10
+ /** The root element. */
11
+ ref?: HTMLSpanElement | null;
12
+ };
13
+ </script>
14
+
15
+ <script lang="ts">
16
+ import { duration } from '../motion/transitions.js';
17
+
18
+ // The reel's line spinner (da3-10): bar, slash, dash, backslash.
19
+ const glyphs = ['|', '/', '-', '\\'];
20
+
21
+ let { label, class: className, ref = $bindable(null), ...rest }: SpinnerProps = $props();
22
+
23
+ let shown = $state(false);
24
+ let step = $state(0);
25
+
26
+ // A timer, not CSS keyframes (keyframes are for expressive entrances
27
+ // only). It reads both durations from the element, so an app can retune
28
+ // them, also per spinner (`style="--spinner-delay: 0ms"`). It keeps
29
+ // stepping under reduced motion: spinners and progress stay.
30
+ function run(node: HTMLElement) {
31
+ const delay = duration(node, '--spinner-delay');
32
+ const every = duration(node, '--spinner-step');
33
+ let interval: ReturnType<typeof setInterval> | undefined;
34
+ const timeout = setTimeout(() => {
35
+ shown = true;
36
+ if (every > 0) interval = setInterval(() => (step = (step + 1) % glyphs.length), every);
37
+ }, delay);
38
+ return () => {
39
+ clearTimeout(timeout);
40
+ clearInterval(interval);
41
+ };
42
+ }
43
+ </script>
44
+
45
+ <!--
46
+ A stepped spinner (BRIEF, visual rule 7): `|`, `/`, `-`, `\` in Geist
47
+ Pixel Square at 26.32px, one state per `--spinner-step` (167ms). Nothing
48
+ shows until `--spinner-delay` (300ms) has passed, so fast work never
49
+ flashes it; the row keeps its height meanwhile. The glyph is aria-hidden
50
+ and sits outside the live region; the label is a `role="status"` region
51
+ that is empty until the spinner appears, so screen readers announce it
52
+ once. Extra attributes and `class` go to the root `span.hp-spinner`, which
53
+ carries `data-state` waiting | visible. Place it start-aligned: centring
54
+ it in a wider box can put the glyph between pixels.
55
+ -->
56
+ <span
57
+ {...rest}
58
+ bind:this={ref}
59
+ class={['hp-spinner', className]}
60
+ data-state={shown ? 'visible' : 'waiting'}
61
+ {@attach run}
62
+ >
63
+ <span class="hp-spinner-glyph" aria-hidden="true" data-step={shown ? step : undefined}
64
+ >{shown ? glyphs[step] : ''}</span
65
+ >
66
+ <span class="hp-spinner-label" role="status">{shown ? label : ''}</span>
67
+ </span>
68
+
69
+ <style>
70
+ /* One row the height of the pixel line, so the spinner does not move
71
+ anything when it appears. The label line sits an even number of pixels
72
+ from the top, so the glyph's top edge stays on a whole pixel. */
73
+ .hp-spinner {
74
+ display: inline-flex;
75
+ align-items: center;
76
+ gap: var(--space-3);
77
+ min-height: var(--type-pixel-leading);
78
+ vertical-align: top;
79
+ }
80
+
81
+ /* Geist Pixel is proportional: at 26.32px (a 1px module) the four glyphs
82
+ advance 6, 11, 10 and 11px. A fixed 12px box keeps the label still
83
+ while the glyph changes. Each glyph starts at the box's edge plus a
84
+ whole-pixel inset that centres it as nearly as whole pixels allow,
85
+ because centring in odd free space would put its edges on half pixels
86
+ (BRIEF, Crisp pixel text). The insets are font metrics. */
87
+ .hp-spinner-glyph {
88
+ flex: none;
89
+ width: 12px;
90
+ font-family: var(--pixel);
91
+ font-size: var(--type-pixel);
92
+ font-weight: 400;
93
+ line-height: var(--type-pixel-leading);
94
+ letter-spacing: 0;
95
+ text-align: start;
96
+ color: var(--text);
97
+ }
98
+ .hp-spinner-glyph[data-step='0'] {
99
+ padding-inline-start: 3px;
100
+ }
101
+ .hp-spinner-glyph[data-step='2'] {
102
+ padding-inline-start: 1px;
103
+ }
104
+
105
+ /* 16px on touch, the dense 14px on fine pointers. */
106
+ .hp-spinner-label {
107
+ color: var(--muted);
108
+ font-size: var(--type-body);
109
+ line-height: 24px;
110
+ }
111
+ @media (pointer: fine) {
112
+ .hp-spinner-label {
113
+ font-size: var(--type-dense);
114
+ line-height: 20px;
115
+ }
116
+ }
117
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export type SpinnerProps = Omit<HTMLAttributes<HTMLSpanElement>, 'children'> & {
3
+ /**
4
+ * What the app is waiting for, such as `Loading projects`. Shown beside
5
+ * the glyph and announced politely once the spinner appears.
6
+ */
7
+ label: string;
8
+ /** The root element. */
9
+ ref?: HTMLSpanElement | null;
10
+ };
11
+ declare const Spinner: import("svelte").Component<SpinnerProps, {}, "ref">;
12
+ type Spinner = ReturnType<typeof Spinner>;
13
+ export default Spinner;
@@ -0,0 +1,253 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+
4
+ /**
5
+ * `standard` (default): the size for a row of several stats. `display`:
6
+ * the 32 to 56px display size, for the view's one display subject.
7
+ */
8
+ export type StatVariant = 'standard' | 'display';
9
+
10
+ /** A status hue for the delta: `ok` good, `warn` needs attention, `danger` bad. */
11
+ export type StatTone = 'ok' | 'warn' | 'danger';
12
+
13
+ export type StatProps = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
14
+ /** The caps label. A fixed string the app defines. */
15
+ label: string;
16
+ /**
17
+ * The value in Geist with tabular figures. A string is shown as given
18
+ * and changes at once. A number is shown through `format`; when it
19
+ * changes after the first render, it ticks from the shown figure to
20
+ * the new one (BRIEF, motion rule 5).
21
+ */
22
+ value: string | number;
23
+ /**
24
+ * Formats a number `value` at rest and at every step of a tick, for
25
+ * example `(n) => n.toLocaleString('en-US')`. Each step is rounded to
26
+ * the larger number of decimal places of the old and new value before
27
+ * it is formatted. Without `format` a number shows as `String(value)`,
28
+ * and the steps show those decimal places.
29
+ */
30
+ format?: (value: number) => string;
31
+ /** A unit after the value, smaller and in the muted ink: `EUR`, `ms`, `tasks`. */
32
+ unit?: string;
33
+ /**
34
+ * The change, in the muted ink. A number gets its sign: `+12`, `−3`
35
+ * (a true minus), `0`. A string is shown as written, sign included.
36
+ */
37
+ delta?: string | number;
38
+ /** What the delta compares with, after it: `since last week`. */
39
+ deltaNote?: string;
40
+ /**
41
+ * Colours the delta in a status hue. Without it the delta stays muted.
42
+ * The label or `deltaNote` must say the same, because colour is never
43
+ * the only signal.
44
+ */
45
+ tone?: StatTone;
46
+ variant?: StatVariant;
47
+ };
48
+ </script>
49
+
50
+ <script lang="ts">
51
+ import { untrack } from 'svelte';
52
+ import { Tween } from 'svelte/motion';
53
+ import { curve, duration, useMotion } from '../motion/index.js';
54
+
55
+ let {
56
+ label,
57
+ value,
58
+ format,
59
+ unit,
60
+ delta,
61
+ deltaNote,
62
+ tone,
63
+ variant = 'standard',
64
+ class: className,
65
+ ...rest
66
+ }: StatProps = $props();
67
+
68
+ const change = $derived(
69
+ typeof delta === 'number'
70
+ ? delta > 0
71
+ ? `+${delta}`
72
+ : delta < 0
73
+ ? `\u2212${-delta}`
74
+ : String(delta)
75
+ : delta
76
+ );
77
+
78
+ // ---- Tick (BRIEF, motion rule 5) ---------------------------------------
79
+
80
+ // A number value that changes after the first render ticks from the
81
+ // figure on screen to the new one. Quiet motion, so it runs in both tiers.
82
+ // Strings, reduced motion and live regions change at once.
83
+ const LIVE = '[aria-live]:not([aria-live="off"]), [role="status"], [role="log"], [role="alert"]';
84
+
85
+ const motion = useMotion();
86
+ let valueEl: HTMLElement | undefined;
87
+ // Starts on the first value, so nothing moves on the first render.
88
+ const tween = new Tween(untrack(() => (typeof value === 'number' ? value : 0)));
89
+ let last = untrack(() => value);
90
+ // The decimal places of the running tick's steps.
91
+ let places = $state(0);
92
+ // The figure on screen when the tick began. A hidden copy keeps its width.
93
+ let from = $state('');
94
+
95
+ /** The decimal places of `n` as `String()` writes it, exponent included. */
96
+ function decimals(n: number): number {
97
+ const [digits, exp = '0'] = String(n).split('e');
98
+ return Math.min(100, Math.max(0, (digits.split('.')[1]?.length ?? 0) - Number(exp)));
99
+ }
100
+ const atRest = (n: number) => (format ? format(n) : String(n));
101
+ /** One step: rounded to `p` places, never `-0`, then formatted. */
102
+ function atStep(n: number, p: number): string {
103
+ const r = Number(n.toFixed(p)) || 0;
104
+ return format ? format(r) : r.toFixed(p);
105
+ }
106
+
107
+ const text = $derived(typeof value === 'number' ? atRest(value) : value);
108
+ const ticking = $derived(
109
+ typeof value === 'number' && !Number.isNaN(value) && tween.current !== value
110
+ );
111
+ const step = $derived(ticking ? atStep(tween.current, places) : '');
112
+
113
+ // Before the DOM updates, so the first frame after a change still shows
114
+ // the old figure. Duration and curve are read from the value element at
115
+ // the change, so a retuned token applies. A change during a tick starts
116
+ // from the step on screen.
117
+ $effect.pre(() => {
118
+ const to = value;
119
+ untrack(() => {
120
+ const was = last;
121
+ last = to;
122
+ if (typeof to !== 'number' || to === was) return;
123
+ const resting = tween.current === tween.target;
124
+ const shown = resting ? tween.current : Number(tween.current.toFixed(places)) || 0;
125
+ const ms =
126
+ valueEl && typeof was === 'number' && !motion.reduced && !valueEl.closest(LIVE)
127
+ ? duration(valueEl, '--dur-tick')
128
+ : 0;
129
+ if (!valueEl || ms <= 0 || !Number.isFinite(shown) || !Number.isFinite(to)) {
130
+ void tween.set(to, { duration: 0 });
131
+ return;
132
+ }
133
+ from = resting ? atRest(shown) : atStep(shown, places);
134
+ places = Math.max(decimals(shown), decimals(to));
135
+ // Tween would run the old tick for one more frame before the new
136
+ // one starts; stop it on the step on screen first.
137
+ if (!resting) void tween.set(shown, { duration: 0 });
138
+ void tween.set(to, { duration: ms, easing: curve(valueEl, '--tick') });
139
+ });
140
+ });
141
+ </script>
142
+
143
+ <!--
144
+ A caps label over a key figure in Geist with tabular figures (BRIEF, visual
145
+ rules 3 and 4; da3-65). A number value ticks to a new value over
146
+ `--dur-tick` on the `--tick` curve, never on the first render. The final
147
+ value stays in the accessible text; the moving steps are aria-hidden. The
148
+ root carries `data-ticking` during a tick. Extra attributes and `class` go
149
+ to the <dl>.
150
+ -->
151
+ <dl
152
+ {...rest}
153
+ class={['hp-stat', className]}
154
+ data-variant={variant}
155
+ data-tone={tone}
156
+ data-ticking={ticking ? '' : undefined}
157
+ >
158
+ <dt class="hp-stat-label hp-label">{label}</dt>
159
+ <dd
160
+ bind:this={valueEl}
161
+ class={['hp-stat-value', variant === 'display' ? 'hp-display' : 'hp-page-title hp-num']}
162
+ >
163
+ <span class="hp-stat-figure"
164
+ ><span class="hp-stat-final">{text}</span>{#if ticking}<span
165
+ class="hp-stat-from"
166
+ aria-hidden="true">{from}</span
167
+ ><span class="hp-stat-step" aria-hidden="true">{step}</span>{/if}</span
168
+ >{#if unit}{' '}<span class="hp-stat-unit">{unit}</span>{/if}
169
+ </dd>
170
+ {#if change !== undefined && change !== ''}
171
+ <dd class="hp-stat-delta">
172
+ <span class="hp-stat-change">{change}</span>{#if deltaNote}{' '}{deltaNote}{/if}
173
+ </dd>
174
+ {/if}
175
+ </dl>
176
+
177
+ <style>
178
+ .hp-stat {
179
+ display: grid;
180
+ align-content: start;
181
+ gap: var(--space-1);
182
+ min-width: 0;
183
+ }
184
+ .hp-stat[data-variant='display'] {
185
+ gap: var(--space-2);
186
+ }
187
+
188
+ .hp-stat-label {
189
+ color: var(--muted);
190
+ }
191
+
192
+ /* Geist even inside a mono context such as a table cell. The size comes
193
+ from base.css: .hp-page-title (24px, 20px on compact) with .hp-num, or
194
+ .hp-display. A long figure breaks rather than widen a phone page. */
195
+ .hp-stat-value {
196
+ font-family: var(--sans);
197
+ overflow-wrap: anywhere;
198
+ }
199
+
200
+ /* During a tick the figure stacks three layers in one grid cell: the
201
+ final value (transparent, read by screen readers), a hidden copy of
202
+ the figure shown when the tick began, and the moving step. The cell is
203
+ as wide as the wider of the old and new figure, so the unit after it
204
+ holds still; the steps lie between the two in value, and with tabular
205
+ figures in width too. */
206
+ .hp-stat[data-ticking] .hp-stat-figure {
207
+ display: inline-grid;
208
+ }
209
+ .hp-stat[data-ticking] .hp-stat-figure > * {
210
+ grid-area: 1 / 1;
211
+ }
212
+ .hp-stat[data-ticking] .hp-stat-final {
213
+ opacity: 0;
214
+ }
215
+ .hp-stat-from {
216
+ visibility: hidden;
217
+ }
218
+
219
+ .hp-stat-unit {
220
+ color: var(--muted);
221
+ font-size: var(--type-body);
222
+ font-weight: 400;
223
+ letter-spacing: 0;
224
+ }
225
+ .hp-stat[data-variant='display'] .hp-stat-unit {
226
+ font-size: var(--type-section);
227
+ }
228
+
229
+ /* 16px on touch, the dense 14px on fine pointers. */
230
+ .hp-stat-delta {
231
+ color: var(--muted);
232
+ font-size: var(--type-body);
233
+ line-height: 24px;
234
+ font-variant-numeric: tabular-nums;
235
+ overflow-wrap: anywhere;
236
+ }
237
+ @media (pointer: fine) {
238
+ .hp-stat-delta {
239
+ font-size: var(--type-dense);
240
+ line-height: 20px;
241
+ }
242
+ }
243
+
244
+ .hp-stat[data-tone='ok'] .hp-stat-change {
245
+ color: var(--ok);
246
+ }
247
+ .hp-stat[data-tone='warn'] .hp-stat-change {
248
+ color: var(--warn);
249
+ }
250
+ .hp-stat[data-tone='danger'] .hp-stat-change {
251
+ color: var(--danger);
252
+ }
253
+ </style>
@@ -0,0 +1,46 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ /**
3
+ * `standard` (default): the size for a row of several stats. `display`:
4
+ * the 32 to 56px display size, for the view's one display subject.
5
+ */
6
+ export type StatVariant = 'standard' | 'display';
7
+ /** A status hue for the delta: `ok` good, `warn` needs attention, `danger` bad. */
8
+ export type StatTone = 'ok' | 'warn' | 'danger';
9
+ export type StatProps = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
10
+ /** The caps label. A fixed string the app defines. */
11
+ label: string;
12
+ /**
13
+ * The value in Geist with tabular figures. A string is shown as given
14
+ * and changes at once. A number is shown through `format`; when it
15
+ * changes after the first render, it ticks from the shown figure to
16
+ * the new one (BRIEF, motion rule 5).
17
+ */
18
+ value: string | number;
19
+ /**
20
+ * Formats a number `value` at rest and at every step of a tick, for
21
+ * example `(n) => n.toLocaleString('en-US')`. Each step is rounded to
22
+ * the larger number of decimal places of the old and new value before
23
+ * it is formatted. Without `format` a number shows as `String(value)`,
24
+ * and the steps show those decimal places.
25
+ */
26
+ format?: (value: number) => string;
27
+ /** A unit after the value, smaller and in the muted ink: `EUR`, `ms`, `tasks`. */
28
+ unit?: string;
29
+ /**
30
+ * The change, in the muted ink. A number gets its sign: `+12`, `−3`
31
+ * (a true minus), `0`. A string is shown as written, sign included.
32
+ */
33
+ delta?: string | number;
34
+ /** What the delta compares with, after it: `since last week`. */
35
+ deltaNote?: string;
36
+ /**
37
+ * Colours the delta in a status hue. Without it the delta stays muted.
38
+ * The label or `deltaNote` must say the same, because colour is never
39
+ * the only signal.
40
+ */
41
+ tone?: StatTone;
42
+ variant?: StatVariant;
43
+ };
44
+ declare const Stat: import("svelte").Component<StatProps, {}, "">;
45
+ type Stat = ReturnType<typeof Stat>;
46
+ export default Stat;
@@ -0,0 +1,189 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLOlAttributes } from 'svelte/elements';
3
+
4
+ export type StepsItem = {
5
+ /** The step's caps label. A short fixed string the app defines. */
6
+ label: string;
7
+ /** A supporting line in `--muted`, as written. */
8
+ detail?: string;
9
+ };
10
+
11
+ /** Where a step stands, from `current`. Each step's `<li>` carries it in `data-state`. */
12
+ export type StepsState = 'done' | 'current' | 'upcoming';
13
+
14
+ /** From which width the steps sit in a row: 600px, 900px or 1200px. */
15
+ export type StepsBreakpoint = 'medium' | 'expanded' | 'large';
16
+
17
+ export type StepsProps = Omit<HTMLOlAttributes, 'children'> & {
18
+ /** In order, first to last. */
19
+ steps: StepsItem[];
20
+ /**
21
+ * The index of the current step, from 0. Steps before it are done,
22
+ * steps after it upcoming. `steps.length` marks every step done.
23
+ */
24
+ current: number;
25
+ /**
26
+ * From which width the steps sit in a row of equal columns; narrower,
27
+ * they stack. Default `medium` (600px). Pick a later one when long
28
+ * labels or many steps would not fit their columns.
29
+ */
30
+ rowFrom?: StepsBreakpoint;
31
+ /** Screen-reader text after a done step's label. Default `done`. */
32
+ doneLabel?: string;
33
+ };
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ let {
38
+ steps,
39
+ current,
40
+ rowFrom = 'medium',
41
+ doneLabel = 'done',
42
+ class: className,
43
+ ...rest
44
+ }: StepsProps = $props();
45
+
46
+ function stateOf(index: number): StepsState {
47
+ return index < current ? 'done' : index === current ? 'current' : 'upcoming';
48
+ }
49
+ </script>
50
+
51
+ <!--
52
+ Ordered steps as discrete segments (BRIEF, visual rule 7): each step has a
53
+ 3px edge, titanium once reached and --border while upcoming, so the edges
54
+ read as a segmented bar. The current step adds the selection tint and
55
+ aria-current="step"; a done step shows a check for its number. A row from
56
+ `rowFrom` (600px by default), stacked below. When the current step
57
+ changes, the edges and the tint fade their colour over --dur-segment.
58
+ Extra attributes (an `aria-label` naming the process)
59
+ and `class` go to the <ol>.
60
+ -->
61
+ <ol role="list" {...rest} class={['hp-steps', className]} data-row-from={rowFrom}>
62
+ {#each steps as step, index}
63
+ {@const state = stateOf(index)}
64
+ <li class="hp-steps-step" data-state={state} aria-current={state === 'current' ? 'step' : undefined}>
65
+ <span class="hp-steps-mark" aria-hidden="true">
66
+ {#if state === 'done'}
67
+ <svg viewBox="0 0 16 16" width="16" height="16" focusable="false">
68
+ <path d="m3 8.5 3.5 3.5 6.5-7.5" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="square" />
69
+ </svg>
70
+ {:else}
71
+ {index + 1}
72
+ {/if}
73
+ </span>
74
+ <span class="hp-steps-label hp-label">{step.label}</span>
75
+ {#if state === 'done'}<span class="hp-visually-hidden">, {doneLabel}</span>{/if}
76
+ {#if step.detail}<span class="hp-steps-detail">{step.detail}</span>{/if}
77
+ </li>
78
+ {/each}
79
+ </ol>
80
+
81
+ <style>
82
+ /* Stacked below 600px. The edges and the current tint reach --bleed into
83
+ the gutter, so the labels keep the page edge, as table rows do. */
84
+ .hp-steps {
85
+ display: grid;
86
+ gap: var(--space-1);
87
+ margin: 0 calc(-1 * var(--bleed));
88
+ padding: 0;
89
+ list-style: none;
90
+ }
91
+
92
+ /* The edge colour is a registered colour property, so it can transition
93
+ on its own while the edge's shape switches at once between the stacked
94
+ and the row form. */
95
+ @property --steps-edge {
96
+ syntax: '<color>';
97
+ inherits: false;
98
+ initial-value: transparent;
99
+ }
100
+
101
+ /* When the current step changes, the edge and the tint fade their colour
102
+ over --dur-segment (BRIEF, short motion). A CSS transition never runs
103
+ on an element's first style, so steps show their state at once when
104
+ they render. 0ms is a snap. Colour only, so reduced motion keeps it.
105
+ The text colour and the number or check change at once. */
106
+ .hp-steps-step {
107
+ --steps-edge: var(--accent);
108
+ position: relative;
109
+ display: grid;
110
+ grid-template-columns: var(--space-4) minmax(0, 1fr);
111
+ column-gap: var(--space-2);
112
+ row-gap: var(--space-1);
113
+ align-content: start;
114
+ padding: var(--space-3) var(--space-3) var(--space-3) var(--bleed);
115
+ /* the brief's 3px selection edge */
116
+ box-shadow: inset 3px 0 0 var(--steps-edge);
117
+ overflow-wrap: anywhere;
118
+ transition:
119
+ --steps-edge var(--dur-segment) var(--settle),
120
+ background-color var(--dur-segment) var(--settle);
121
+ }
122
+ .hp-steps-step[data-state='current'] {
123
+ background: var(--surface-selected);
124
+ }
125
+ .hp-steps-step[data-state='upcoming'] {
126
+ --steps-edge: var(--border);
127
+ color: var(--muted);
128
+ }
129
+
130
+ /* The number or the check, in a 16px box beside the label's first line. */
131
+ .hp-steps-mark {
132
+ display: grid;
133
+ place-items: center;
134
+ height: var(--space-4);
135
+ font-size: var(--type-label);
136
+ font-weight: 500;
137
+ line-height: 16px;
138
+ font-variant-numeric: tabular-nums;
139
+ }
140
+
141
+ .hp-steps-detail {
142
+ grid-column: 2;
143
+ color: var(--muted);
144
+ font-size: var(--type-body);
145
+ line-height: 24px;
146
+ }
147
+ @media (pointer: fine) {
148
+ .hp-steps-detail {
149
+ font-size: var(--type-dense);
150
+ line-height: 20px;
151
+ }
152
+ }
153
+
154
+ /* A row of equal columns from the rowFrom width, the edge on top. A label
155
+ longer than its column still breaks rather than widen the page. */
156
+ @media (min-width: 600px) {
157
+ .hp-steps[data-row-from='medium'] {
158
+ grid-auto-columns: minmax(0, 1fr);
159
+ grid-auto-flow: column;
160
+ }
161
+ [data-row-from='medium'] > .hp-steps-step {
162
+ padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
163
+ /* the brief's 3px selection edge */
164
+ box-shadow: inset 0 3px 0 var(--steps-edge);
165
+ }
166
+ }
167
+ @media (min-width: 900px) {
168
+ .hp-steps[data-row-from='expanded'] {
169
+ grid-auto-columns: minmax(0, 1fr);
170
+ grid-auto-flow: column;
171
+ }
172
+ [data-row-from='expanded'] > .hp-steps-step {
173
+ padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
174
+ /* the brief's 3px selection edge */
175
+ box-shadow: inset 0 3px 0 var(--steps-edge);
176
+ }
177
+ }
178
+ @media (min-width: 1200px) {
179
+ .hp-steps[data-row-from='large'] {
180
+ grid-auto-columns: minmax(0, 1fr);
181
+ grid-auto-flow: column;
182
+ }
183
+ [data-row-from='large'] > .hp-steps-step {
184
+ padding: var(--space-4) var(--space-2) var(--space-3) var(--bleed);
185
+ /* the brief's 3px selection edge */
186
+ box-shadow: inset 0 3px 0 var(--steps-edge);
187
+ }
188
+ }
189
+ </style>
@@ -0,0 +1,31 @@
1
+ import type { HTMLOlAttributes } from 'svelte/elements';
2
+ export type StepsItem = {
3
+ /** The step's caps label. A short fixed string the app defines. */
4
+ label: string;
5
+ /** A supporting line in `--muted`, as written. */
6
+ detail?: string;
7
+ };
8
+ /** Where a step stands, from `current`. Each step's `<li>` carries it in `data-state`. */
9
+ export type StepsState = 'done' | 'current' | 'upcoming';
10
+ /** From which width the steps sit in a row: 600px, 900px or 1200px. */
11
+ export type StepsBreakpoint = 'medium' | 'expanded' | 'large';
12
+ export type StepsProps = Omit<HTMLOlAttributes, 'children'> & {
13
+ /** In order, first to last. */
14
+ steps: StepsItem[];
15
+ /**
16
+ * The index of the current step, from 0. Steps before it are done,
17
+ * steps after it upcoming. `steps.length` marks every step done.
18
+ */
19
+ current: number;
20
+ /**
21
+ * From which width the steps sit in a row of equal columns; narrower,
22
+ * they stack. Default `medium` (600px). Pick a later one when long
23
+ * labels or many steps would not fit their columns.
24
+ */
25
+ rowFrom?: StepsBreakpoint;
26
+ /** Screen-reader text after a done step's label. Default `done`. */
27
+ doneLabel?: string;
28
+ };
29
+ declare const Steps: import("svelte").Component<StepsProps, {}, "">;
30
+ type Steps = ReturnType<typeof Steps>;
31
+ export default Steps;