@mystravil/hardpoint 0.2.0 → 0.3.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 (54) hide show
  1. package/CHANGELOG.md +51 -38
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -625
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +221 -174
  7. package/dist/components/Button.svelte.d.ts +18 -2
  8. package/dist/components/Checkbox.svelte +235 -235
  9. package/dist/components/CheckboxGroup.svelte +207 -207
  10. package/dist/components/Combobox.svelte +901 -901
  11. package/dist/components/Dialog.svelte +359 -359
  12. package/dist/components/Disclosure.svelte +201 -201
  13. package/dist/components/EmptyState.svelte +88 -88
  14. package/dist/components/ErrorState.svelte +135 -135
  15. package/dist/components/ImageViewer.svelte +764 -764
  16. package/dist/components/KeyValueList.svelte +114 -114
  17. package/dist/components/ListDetail.svelte +218 -218
  18. package/dist/components/LoadRegion.svelte +219 -219
  19. package/dist/components/MediaFrame.svelte +110 -110
  20. package/dist/components/MediaImage.svelte +256 -256
  21. package/dist/components/Menu.svelte +275 -258
  22. package/dist/components/MenuCheckboxItem.svelte +119 -119
  23. package/dist/components/MenuGroup.svelte +50 -50
  24. package/dist/components/MenuItem.svelte +94 -94
  25. package/dist/components/MenuRadioGroup.svelte +62 -62
  26. package/dist/components/MenuRadioItem.svelte +110 -110
  27. package/dist/components/MenuSeparator.svelte +28 -28
  28. package/dist/components/Meter.svelte +283 -283
  29. package/dist/components/NativeSelect.svelte +135 -135
  30. package/dist/components/Notice.svelte +262 -262
  31. package/dist/components/Popover.svelte +222 -222
  32. package/dist/components/Progress.svelte +157 -157
  33. package/dist/components/RadioGroup.svelte +440 -440
  34. package/dist/components/Select.svelte +471 -471
  35. package/dist/components/Skeleton.svelte +149 -149
  36. package/dist/components/Spinner.svelte +118 -118
  37. package/dist/components/Stat.svelte +253 -253
  38. package/dist/components/Steps.svelte +189 -189
  39. package/dist/components/Switch.svelte +222 -222
  40. package/dist/components/Table.svelte +504 -504
  41. package/dist/components/Tabs.svelte +246 -246
  42. package/dist/components/Tag.svelte +40 -40
  43. package/dist/components/TextArea.svelte +272 -272
  44. package/dist/components/TextField.svelte +396 -256
  45. package/dist/components/TextField.svelte.d.ts +16 -1
  46. package/dist/components/Timeline.svelte +156 -156
  47. package/dist/components/Toaster.svelte +411 -411
  48. package/dist/components/Tooltip.svelte +200 -200
  49. package/dist/components/TooltipGroup.svelte +46 -46
  50. package/dist/components/internal/Scrim.svelte +52 -52
  51. package/dist/components/internal/Sheet.svelte +263 -263
  52. package/dist/fonts.css +71 -71
  53. package/dist/tokens.css +283 -283
  54. package/package.json +81 -76
@@ -1,256 +1,256 @@
1
- <script lang="ts" module>
2
- import type { HTMLImgAttributes } from 'svelte/elements';
3
-
4
- export type MediaImageProps = Omit<HTMLImgAttributes, 'src' | 'alt' | 'children'> & {
5
- /**
6
- * The image's URL. Leave it undefined while the app does not know it
7
- * yet: the frame waits, as it does while the image loads. A different
8
- * URL starts a new load; the same one changes nothing.
9
- */
10
- src?: string;
11
- /** The text alternative, required. `""` for a decorative image. */
12
- alt: string;
13
- /** The `<img>` element, while there is a `src`. Bindable, read-only. */
14
- ref?: HTMLImageElement | null;
15
- };
16
- </script>
17
-
18
- <script lang="ts">
19
- import { untrack } from 'svelte';
20
- import { useMotion } from '../motion/context.js';
21
- import { duration } from '../motion/transitions.js';
22
- import Skeleton from './Skeleton.svelte';
23
- import { resolveImage } from './internal/resolve.js';
24
-
25
- let { src, alt, ref = $bindable(null), class: className, ...rest }: MediaImageProps = $props();
26
-
27
- const motion = useMotion();
28
-
29
- // waiting: no src yet, or the image has not loaded; the block shows
30
- // after --skeleton-delay. fade and resolve: the image reveals over the
31
- // block. shown: the image alone. error: the image failed to load.
32
- type Phase = 'waiting' | 'fade' | 'resolve' | 'shown' | 'error';
33
- let phase = $state<Phase>('waiting');
34
-
35
- let root = $state<HTMLDivElement>();
36
- /** The src the phase belongs to. */
37
- let current = untrack(() => src);
38
- /** The image a resolve draws, and its step in ms, read when it arrived. */
39
- let arrived: HTMLImageElement | undefined;
40
- let step = 0;
41
-
42
- // Before the DOM update, so the image of a new src starts hidden. A block
43
- // already on screen stays: the wait goes on with the new src.
44
- $effect.pre(() => {
45
- const next = src;
46
- untrack(() => {
47
- if (next === current) return;
48
- current = next;
49
- phase = 'waiting';
50
- });
51
- });
52
-
53
- // One <img> per src (see the {#key} below), so this runs once per load.
54
- // The app's own onload and onerror sit on the element through `rest` and
55
- // fire as usual.
56
- function watch(img: HTMLImageElement) {
57
- return untrack(() => {
58
- let alive = true;
59
- let pending = false;
60
- let cancel: (() => void) | undefined;
61
-
62
- const fail = () => {
63
- if (alive && phase === 'waiting') phase = 'error';
64
- };
65
-
66
- // The image is decoded, so its first frame on screen is never blank.
67
- const arrive = () => {
68
- if (!alive || phase !== 'waiting') return;
69
- const node = root;
70
- // Before the block showed, or with no box to reveal in: at once.
71
- const block = node?.querySelector(':scope > .hp-skeleton');
72
- if (!node || block?.getAttribute('data-state') !== 'visible' || !node.clientWidth || !node.clientHeight) {
73
- phase = 'shown';
74
- return;
75
- }
76
- // Read now, so a token retuned during the load applies.
77
- if (motion.expressive && img.naturalWidth > 0) {
78
- step = duration(node, '--resolve-step');
79
- if (step > 0) {
80
- arrived = img;
81
- phase = 'resolve';
82
- return;
83
- }
84
- }
85
- const ms = duration(node, '--dur-reveal');
86
- if (ms <= 0) {
87
- phase = 'shown';
88
- return;
89
- }
90
- const settle = getComputedStyle(node).getPropertyValue('--settle').trim();
91
- // Started before the DOM update, so the image's first frame is
92
- // already at 0 over the block.
93
- const fade = img.animate(
94
- { opacity: [0, 1] },
95
- { duration: ms, easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out' }
96
- );
97
- fade.onfinish = () => {
98
- if (alive) phase = 'shown';
99
- };
100
- cancel = () => fade.cancel();
101
- phase = 'fade';
102
- };
103
-
104
- const onload = () => {
105
- // Only the first load counts: a later one is a new srcset
106
- // candidate, or an event Svelte replays after hydration.
107
- if (pending || phase !== 'waiting') return;
108
- pending = true;
109
- img.decode().then(arrive, () => {
110
- if (img.naturalWidth > 0) arrive();
111
- else fail();
112
- });
113
- };
114
-
115
- if (img.complete) {
116
- // Cached, or loaded before hydration: at once. A complete image
117
- // without a size is broken, unless it is an SVG without one.
118
- if (img.naturalWidth > 0) phase = 'shown';
119
- else
120
- img.decode().then(() => {
121
- if (alive && phase === 'waiting') phase = 'shown';
122
- }, fail);
123
- }
124
- img.addEventListener('load', onload);
125
- img.addEventListener('error', fail);
126
- return () => {
127
- alive = false;
128
- cancel?.();
129
- img.removeEventListener('load', onload);
130
- img.removeEventListener('error', fail);
131
- };
132
- });
133
- }
134
-
135
- // The canvas mounts with the resolve, after the DOM update and before it
136
- // is painted; it unmounts when the resolve ends or a new src starts.
137
- function mosaic(canvas: HTMLCanvasElement) {
138
- return untrack(() => {
139
- const img = arrived;
140
- if (!img) {
141
- phase = 'shown';
142
- return;
143
- }
144
- return resolveImage(canvas, img, step, () => {
145
- if (phase === 'resolve') phase = 'shown';
146
- });
147
- });
148
- }
149
- </script>
150
-
151
- <!--
152
- An image that fills a MediaFrame and takes over from a skeleton block
153
- when it loads (BRIEF, motion rules 2 and 10):
154
-
155
- <MediaFrame ratio="3 / 2"><MediaImage src={url} alt="…" /></MediaFrame>
156
-
157
- It lies over the frame's whole inner box (absolute, inset 0) and covers
158
- it (`object-fit: cover`, the only fit for now: a frame crops, it never
159
- letterboxes). So the frame needs a size of its own: a `ratio`, or
160
- `--media-frame-ratio`. A frame without one takes its content's height,
161
- and this has none, so the frame shrinks to its two border lines.
162
-
163
- While there is no `src` or the image has not loaded, a block Skeleton
164
- fills the frame; it appears after `--skeleton-delay`, as everywhere else.
165
- An image already complete when it mounts (cached, or loaded before
166
- hydration), or one that arrives before the block showed, appears at
167
- once. Otherwise it reveals once `img.decode()` is done, so its first
168
- frame is never blank: in the expressive tier (`useMotion().expressive`)
169
- it resolves from a mosaic that sharpens in five steps of `--resolve-step`
170
- (internal/resolve.ts); everywhere else, and so with reduced motion, it
171
- fades in over the block for `--dur-reveal` on `--settle`, as LoadRegion's
172
- crossfade does. The fade changes only opacity, so it runs under reduced
173
- motion. `--resolve-step`, `--dur-reveal` and `--settle` are read from
174
- the element when the image arrives. An image from another origin
175
- without CORS headers resolves too: the mosaic is drawn, never read. An
176
- image rendered on the server stays hidden until hydration.
177
-
178
- On a load error the block goes and no broken-image glyph shows: the
179
- frame shows the `alt` as a muted caption in its middle, so a sighted
180
- user still learns what the image held, or nothing for `alt=""`. The
181
- app's `onerror` fires; a different `src` tries again.
182
-
183
- The <img> with its `alt` is in the DOM and the accessibility tree from
184
- the moment it has a `src`; while it waits it is hidden by opacity only.
185
- The block, the canvas and the caption are `aria-hidden`, and pointer
186
- events pass through them. No live region: the page says what loads. It
187
- never takes focus. `class` and other attributes go to the <img>
188
- (`srcset`, `sizes`, `loading`, `width`, `height`, handlers). The root
189
- `div.hp-media-image` carries `data-state` waiting | fade | resolve |
190
- shown | error.
191
- -->
192
- <div class="hp-media-image" data-state={phase} bind:this={root}>
193
- {#if phase === 'waiting' || phase === 'fade' || phase === 'resolve'}
194
- <Skeleton variant="block" height="100%" />
195
- {/if}
196
- {#if src}
197
- {#key src}
198
- <img {...rest} {src} {alt} class={className} bind:this={ref} {@attach watch} />
199
- {/key}
200
- {/if}
201
- {#if phase === 'resolve'}
202
- <canvas aria-hidden="true" {@attach mosaic}></canvas>
203
- {/if}
204
- {#if phase === 'error' && alt}
205
- <span class="caption hp-caption" aria-hidden="true">{alt}</span>
206
- {/if}
207
- </div>
208
-
209
- <style>
210
- /* Over the frame's inner box; the frame's marks paint over it. */
211
- .hp-media-image {
212
- position: absolute;
213
- inset: 0;
214
- overflow: hidden;
215
- }
216
- .hp-media-image > :global(.hp-skeleton),
217
- canvas {
218
- pointer-events: none;
219
- }
220
- img,
221
- canvas {
222
- position: absolute;
223
- inset: 0;
224
- display: block;
225
- width: 100%;
226
- height: 100%;
227
- }
228
- img {
229
- object-fit: cover;
230
- }
231
- /* Hidden by opacity only, so the image stays in the accessibility tree.
232
- A broken image would draw the browser's glyph and its alt. */
233
- [data-state='waiting'] > img,
234
- [data-state='resolve'] > img,
235
- [data-state='error'] > img {
236
- opacity: 0;
237
- }
238
- /* Nearest neighbour if the box does not fill whole device pixels: the
239
- blocks stay square and crisp, never blurred. */
240
- canvas {
241
- image-rendering: pixelated;
242
- }
243
-
244
- /* The alt of an image that failed, centred, clear of the frame's marks. */
245
- .caption {
246
- position: absolute;
247
- inset: 0;
248
- display: grid;
249
- place-items: center;
250
- padding: var(--space-8);
251
- overflow: hidden;
252
- color: var(--muted);
253
- text-align: center;
254
- pointer-events: none;
255
- }
256
- </style>
1
+ <script lang="ts" module>
2
+ import type { HTMLImgAttributes } from 'svelte/elements';
3
+
4
+ export type MediaImageProps = Omit<HTMLImgAttributes, 'src' | 'alt' | 'children'> & {
5
+ /**
6
+ * The image's URL. Leave it undefined while the app does not know it
7
+ * yet: the frame waits, as it does while the image loads. A different
8
+ * URL starts a new load; the same one changes nothing.
9
+ */
10
+ src?: string;
11
+ /** The text alternative, required. `""` for a decorative image. */
12
+ alt: string;
13
+ /** The `<img>` element, while there is a `src`. Bindable, read-only. */
14
+ ref?: HTMLImageElement | null;
15
+ };
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { untrack } from 'svelte';
20
+ import { useMotion } from '../motion/context.js';
21
+ import { duration } from '../motion/transitions.js';
22
+ import Skeleton from './Skeleton.svelte';
23
+ import { resolveImage } from './internal/resolve.js';
24
+
25
+ let { src, alt, ref = $bindable(null), class: className, ...rest }: MediaImageProps = $props();
26
+
27
+ const motion = useMotion();
28
+
29
+ // waiting: no src yet, or the image has not loaded; the block shows
30
+ // after --skeleton-delay. fade and resolve: the image reveals over the
31
+ // block. shown: the image alone. error: the image failed to load.
32
+ type Phase = 'waiting' | 'fade' | 'resolve' | 'shown' | 'error';
33
+ let phase = $state<Phase>('waiting');
34
+
35
+ let root = $state<HTMLDivElement>();
36
+ /** The src the phase belongs to. */
37
+ let current = untrack(() => src);
38
+ /** The image a resolve draws, and its step in ms, read when it arrived. */
39
+ let arrived: HTMLImageElement | undefined;
40
+ let step = 0;
41
+
42
+ // Before the DOM update, so the image of a new src starts hidden. A block
43
+ // already on screen stays: the wait goes on with the new src.
44
+ $effect.pre(() => {
45
+ const next = src;
46
+ untrack(() => {
47
+ if (next === current) return;
48
+ current = next;
49
+ phase = 'waiting';
50
+ });
51
+ });
52
+
53
+ // One <img> per src (see the {#key} below), so this runs once per load.
54
+ // The app's own onload and onerror sit on the element through `rest` and
55
+ // fire as usual.
56
+ function watch(img: HTMLImageElement) {
57
+ return untrack(() => {
58
+ let alive = true;
59
+ let pending = false;
60
+ let cancel: (() => void) | undefined;
61
+
62
+ const fail = () => {
63
+ if (alive && phase === 'waiting') phase = 'error';
64
+ };
65
+
66
+ // The image is decoded, so its first frame on screen is never blank.
67
+ const arrive = () => {
68
+ if (!alive || phase !== 'waiting') return;
69
+ const node = root;
70
+ // Before the block showed, or with no box to reveal in: at once.
71
+ const block = node?.querySelector(':scope > .hp-skeleton');
72
+ if (!node || block?.getAttribute('data-state') !== 'visible' || !node.clientWidth || !node.clientHeight) {
73
+ phase = 'shown';
74
+ return;
75
+ }
76
+ // Read now, so a token retuned during the load applies.
77
+ if (motion.expressive && img.naturalWidth > 0) {
78
+ step = duration(node, '--resolve-step');
79
+ if (step > 0) {
80
+ arrived = img;
81
+ phase = 'resolve';
82
+ return;
83
+ }
84
+ }
85
+ const ms = duration(node, '--dur-reveal');
86
+ if (ms <= 0) {
87
+ phase = 'shown';
88
+ return;
89
+ }
90
+ const settle = getComputedStyle(node).getPropertyValue('--settle').trim();
91
+ // Started before the DOM update, so the image's first frame is
92
+ // already at 0 over the block.
93
+ const fade = img.animate(
94
+ { opacity: [0, 1] },
95
+ { duration: ms, easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out' }
96
+ );
97
+ fade.onfinish = () => {
98
+ if (alive) phase = 'shown';
99
+ };
100
+ cancel = () => fade.cancel();
101
+ phase = 'fade';
102
+ };
103
+
104
+ const onload = () => {
105
+ // Only the first load counts: a later one is a new srcset
106
+ // candidate, or an event Svelte replays after hydration.
107
+ if (pending || phase !== 'waiting') return;
108
+ pending = true;
109
+ img.decode().then(arrive, () => {
110
+ if (img.naturalWidth > 0) arrive();
111
+ else fail();
112
+ });
113
+ };
114
+
115
+ if (img.complete) {
116
+ // Cached, or loaded before hydration: at once. A complete image
117
+ // without a size is broken, unless it is an SVG without one.
118
+ if (img.naturalWidth > 0) phase = 'shown';
119
+ else
120
+ img.decode().then(() => {
121
+ if (alive && phase === 'waiting') phase = 'shown';
122
+ }, fail);
123
+ }
124
+ img.addEventListener('load', onload);
125
+ img.addEventListener('error', fail);
126
+ return () => {
127
+ alive = false;
128
+ cancel?.();
129
+ img.removeEventListener('load', onload);
130
+ img.removeEventListener('error', fail);
131
+ };
132
+ });
133
+ }
134
+
135
+ // The canvas mounts with the resolve, after the DOM update and before it
136
+ // is painted; it unmounts when the resolve ends or a new src starts.
137
+ function mosaic(canvas: HTMLCanvasElement) {
138
+ return untrack(() => {
139
+ const img = arrived;
140
+ if (!img) {
141
+ phase = 'shown';
142
+ return;
143
+ }
144
+ return resolveImage(canvas, img, step, () => {
145
+ if (phase === 'resolve') phase = 'shown';
146
+ });
147
+ });
148
+ }
149
+ </script>
150
+
151
+ <!--
152
+ An image that fills a MediaFrame and takes over from a skeleton block
153
+ when it loads (BRIEF, motion rules 2 and 10):
154
+
155
+ <MediaFrame ratio="3 / 2"><MediaImage src={url} alt="…" /></MediaFrame>
156
+
157
+ It lies over the frame's whole inner box (absolute, inset 0) and covers
158
+ it (`object-fit: cover`, the only fit for now: a frame crops, it never
159
+ letterboxes). So the frame needs a size of its own: a `ratio`, or
160
+ `--media-frame-ratio`. A frame without one takes its content's height,
161
+ and this has none, so the frame shrinks to its two border lines.
162
+
163
+ While there is no `src` or the image has not loaded, a block Skeleton
164
+ fills the frame; it appears after `--skeleton-delay`, as everywhere else.
165
+ An image already complete when it mounts (cached, or loaded before
166
+ hydration), or one that arrives before the block showed, appears at
167
+ once. Otherwise it reveals once `img.decode()` is done, so its first
168
+ frame is never blank: in the expressive tier (`useMotion().expressive`)
169
+ it resolves from a mosaic that sharpens in five steps of `--resolve-step`
170
+ (internal/resolve.ts); everywhere else, and so with reduced motion, it
171
+ fades in over the block for `--dur-reveal` on `--settle`, as LoadRegion's
172
+ crossfade does. The fade changes only opacity, so it runs under reduced
173
+ motion. `--resolve-step`, `--dur-reveal` and `--settle` are read from
174
+ the element when the image arrives. An image from another origin
175
+ without CORS headers resolves too: the mosaic is drawn, never read. An
176
+ image rendered on the server stays hidden until hydration.
177
+
178
+ On a load error the block goes and no broken-image glyph shows: the
179
+ frame shows the `alt` as a muted caption in its middle, so a sighted
180
+ user still learns what the image held, or nothing for `alt=""`. The
181
+ app's `onerror` fires; a different `src` tries again.
182
+
183
+ The <img> with its `alt` is in the DOM and the accessibility tree from
184
+ the moment it has a `src`; while it waits it is hidden by opacity only.
185
+ The block, the canvas and the caption are `aria-hidden`, and pointer
186
+ events pass through them. No live region: the page says what loads. It
187
+ never takes focus. `class` and other attributes go to the <img>
188
+ (`srcset`, `sizes`, `loading`, `width`, `height`, handlers). The root
189
+ `div.hp-media-image` carries `data-state` waiting | fade | resolve |
190
+ shown | error.
191
+ -->
192
+ <div class="hp-media-image" data-state={phase} bind:this={root}>
193
+ {#if phase === 'waiting' || phase === 'fade' || phase === 'resolve'}
194
+ <Skeleton variant="block" height="100%" />
195
+ {/if}
196
+ {#if src}
197
+ {#key src}
198
+ <img {...rest} {src} {alt} class={className} bind:this={ref} {@attach watch} />
199
+ {/key}
200
+ {/if}
201
+ {#if phase === 'resolve'}
202
+ <canvas aria-hidden="true" {@attach mosaic}></canvas>
203
+ {/if}
204
+ {#if phase === 'error' && alt}
205
+ <span class="caption hp-caption" aria-hidden="true">{alt}</span>
206
+ {/if}
207
+ </div>
208
+
209
+ <style>
210
+ /* Over the frame's inner box; the frame's marks paint over it. */
211
+ .hp-media-image {
212
+ position: absolute;
213
+ inset: 0;
214
+ overflow: hidden;
215
+ }
216
+ .hp-media-image > :global(.hp-skeleton),
217
+ canvas {
218
+ pointer-events: none;
219
+ }
220
+ img,
221
+ canvas {
222
+ position: absolute;
223
+ inset: 0;
224
+ display: block;
225
+ width: 100%;
226
+ height: 100%;
227
+ }
228
+ img {
229
+ object-fit: cover;
230
+ }
231
+ /* Hidden by opacity only, so the image stays in the accessibility tree.
232
+ A broken image would draw the browser's glyph and its alt. */
233
+ [data-state='waiting'] > img,
234
+ [data-state='resolve'] > img,
235
+ [data-state='error'] > img {
236
+ opacity: 0;
237
+ }
238
+ /* Nearest neighbour if the box does not fill whole device pixels: the
239
+ blocks stay square and crisp, never blurred. */
240
+ canvas {
241
+ image-rendering: pixelated;
242
+ }
243
+
244
+ /* The alt of an image that failed, centred, clear of the frame's marks. */
245
+ .caption {
246
+ position: absolute;
247
+ inset: 0;
248
+ display: grid;
249
+ place-items: center;
250
+ padding: var(--space-8);
251
+ overflow: hidden;
252
+ color: var(--muted);
253
+ text-align: center;
254
+ pointer-events: none;
255
+ }
256
+ </style>