@mystravil/hardpoint 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/CHANGELOG.md +42 -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 +174 -174
  7. package/dist/components/Checkbox.svelte +235 -235
  8. package/dist/components/CheckboxGroup.svelte +207 -207
  9. package/dist/components/Combobox.svelte +901 -901
  10. package/dist/components/Dialog.svelte +359 -359
  11. package/dist/components/Disclosure.svelte +201 -201
  12. package/dist/components/EmptyState.svelte +88 -88
  13. package/dist/components/ErrorState.svelte +135 -135
  14. package/dist/components/ImageViewer.svelte +764 -764
  15. package/dist/components/KeyValueList.svelte +114 -114
  16. package/dist/components/ListDetail.svelte +218 -218
  17. package/dist/components/LoadRegion.svelte +219 -219
  18. package/dist/components/MediaFrame.svelte +110 -110
  19. package/dist/components/MediaImage.svelte +256 -256
  20. package/dist/components/Menu.svelte +258 -258
  21. package/dist/components/MenuCheckboxItem.svelte +119 -119
  22. package/dist/components/MenuGroup.svelte +50 -50
  23. package/dist/components/MenuItem.svelte +94 -94
  24. package/dist/components/MenuRadioGroup.svelte +62 -62
  25. package/dist/components/MenuRadioItem.svelte +110 -110
  26. package/dist/components/MenuSeparator.svelte +28 -28
  27. package/dist/components/Meter.svelte +283 -283
  28. package/dist/components/NativeSelect.svelte +135 -135
  29. package/dist/components/Notice.svelte +262 -262
  30. package/dist/components/Popover.svelte +222 -222
  31. package/dist/components/Progress.svelte +157 -157
  32. package/dist/components/RadioGroup.svelte +440 -440
  33. package/dist/components/Select.svelte +471 -471
  34. package/dist/components/Skeleton.svelte +149 -149
  35. package/dist/components/Spinner.svelte +118 -118
  36. package/dist/components/Stat.svelte +253 -253
  37. package/dist/components/Steps.svelte +189 -189
  38. package/dist/components/Switch.svelte +222 -222
  39. package/dist/components/Table.svelte +504 -504
  40. package/dist/components/Tabs.svelte +246 -246
  41. package/dist/components/Tag.svelte +40 -40
  42. package/dist/components/TextArea.svelte +272 -272
  43. package/dist/components/TextField.svelte +256 -256
  44. package/dist/components/Timeline.svelte +156 -156
  45. package/dist/components/Toaster.svelte +411 -411
  46. package/dist/components/Tooltip.svelte +200 -200
  47. package/dist/components/TooltipGroup.svelte +46 -46
  48. package/dist/components/internal/Scrim.svelte +52 -52
  49. package/dist/components/internal/Sheet.svelte +263 -263
  50. package/dist/fonts.css +71 -71
  51. package/dist/tokens.css +283 -283
  52. 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>