@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.
- package/CHANGELOG.md +42 -38
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -625
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -235
- package/dist/components/CheckboxGroup.svelte +207 -207
- package/dist/components/Combobox.svelte +901 -901
- package/dist/components/Dialog.svelte +359 -359
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -764
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -218
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -283
- package/dist/components/NativeSelect.svelte +135 -135
- package/dist/components/Notice.svelte +262 -262
- package/dist/components/Popover.svelte +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -440
- package/dist/components/Select.svelte +471 -471
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -118
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -272
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/fonts.css +71 -71
- package/dist/tokens.css +283 -283
- package/package.json +81 -76
|
@@ -1,219 +1,219 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
|
|
5
|
-
export type LoadRegionProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'placeholder'> & {
|
|
6
|
-
/** True while the content loads: the region shows `placeholder` and is `aria-busy`. */
|
|
7
|
-
loading: boolean;
|
|
8
|
-
/**
|
|
9
|
-
* The text of the status line, such as `Loading tasks`, then `5 tasks`.
|
|
10
|
-
* The line is visually hidden, stays mounted outside the region and is
|
|
11
|
-
* a `role="status"` region, so each change is announced. Leave it empty
|
|
12
|
-
* while an error state announces itself.
|
|
13
|
-
*/
|
|
14
|
-
status: string;
|
|
15
|
-
/**
|
|
16
|
-
* The skeleton layout shown while loading, rendered inside an
|
|
17
|
-
* `aria-hidden`, `inert` wrapper. Give it the content's own layout
|
|
18
|
-
* container: during a reveal it lies under the content in a box of its
|
|
19
|
-
* own, so a grid or gap set on the region would not reach it.
|
|
20
|
-
*/
|
|
21
|
-
placeholder: Snippet;
|
|
22
|
-
/** The content, and also any empty or error state. */
|
|
23
|
-
children: Snippet;
|
|
24
|
-
/** The region element. A page moves focus here when the content replaces the focused control. */
|
|
25
|
-
ref?: HTMLDivElement | null;
|
|
26
|
-
};
|
|
27
|
-
</script>
|
|
28
|
-
|
|
29
|
-
<script lang="ts">
|
|
30
|
-
import { untrack } from 'svelte';
|
|
31
|
-
import { duration } from '../motion/transitions.js';
|
|
32
|
-
|
|
33
|
-
let {
|
|
34
|
-
loading,
|
|
35
|
-
status,
|
|
36
|
-
placeholder,
|
|
37
|
-
children,
|
|
38
|
-
ref = $bindable(null),
|
|
39
|
-
class: className,
|
|
40
|
-
...rest
|
|
41
|
-
}: LoadRegionProps = $props();
|
|
42
|
-
|
|
43
|
-
// ---- Reveal (BRIEF, motion rule 2) -------------------------------------
|
|
44
|
-
|
|
45
|
-
// When a load ends after its skeletons became visible and --dur-reveal is
|
|
46
|
-
// more than 0ms, the content crossfades in over the skeletons for
|
|
47
|
-
// --dur-reveal on --settle. The placeholder stays mounted under the
|
|
48
|
-
// content, unchanged. The content lies on top on an opaque backdrop (the
|
|
49
|
-
// colour behind the region) and fades in from 0, so every pixel moves in
|
|
50
|
-
// a straight line from the placeholder's to the content's: a bar turns
|
|
51
|
-
// into its text, and a part drawn the same in both layers (a tag, a
|
|
52
|
-
// label, a table's band) does not change at all. Then the placeholder
|
|
53
|
-
// goes. At 0ms, for a load that ends within --skeleton-delay, on the
|
|
54
|
-
// first render and for any change that is not a load ending, the content
|
|
55
|
-
// replaces the placeholder in one frame. Opacity only: the skeletons
|
|
56
|
-
// mirror the layout, so nothing moves. Quiet motion, so it runs in both
|
|
57
|
-
// tiers; it is only a fade, so reduced motion keeps it.
|
|
58
|
-
|
|
59
|
-
type Phase = 'loading' | 'reveal' | 'content';
|
|
60
|
-
let phase = $state<Phase>(untrack(() => (loading ? 'loading' : 'content')));
|
|
61
|
-
// Counts loads. A new load remounts the placeholder, so its skeletons
|
|
62
|
-
// wait out --skeleton-delay again, also when it cuts a reveal short.
|
|
63
|
-
let load = $state(0);
|
|
64
|
-
|
|
65
|
-
/** When the running load began, or null between loads. */
|
|
66
|
-
let began: number | null = untrack(() => (loading ? performance.now() : null));
|
|
67
|
-
/** --skeleton-delay when the load began, as the skeletons read it; null until read. */
|
|
68
|
-
let delay: number | null = null;
|
|
69
|
-
/** The running reveal's timing, read from the region when the load ended. */
|
|
70
|
-
let timing: KeyframeAnimationOptions = {};
|
|
71
|
-
|
|
72
|
-
// Before the DOM update, so a reveal keeps the placeholder mounted (its
|
|
73
|
-
// skeletons stay visible) while the content mounts on top. Only `loading`
|
|
74
|
-
// is tracked.
|
|
75
|
-
$effect.pre(() => {
|
|
76
|
-
const busy = loading;
|
|
77
|
-
untrack(() => {
|
|
78
|
-
const node = ref;
|
|
79
|
-
if (busy) {
|
|
80
|
-
if (phase !== 'loading') {
|
|
81
|
-
began = performance.now();
|
|
82
|
-
delay = node ? duration(node, '--skeleton-delay') : null;
|
|
83
|
-
load += 1;
|
|
84
|
-
phase = 'loading';
|
|
85
|
-
}
|
|
86
|
-
return;
|
|
87
|
-
}
|
|
88
|
-
if (phase !== 'loading') return;
|
|
89
|
-
const elapsed = began === null ? -1 : performance.now() - began;
|
|
90
|
-
began = null;
|
|
91
|
-
// Read now, so a token retuned during the load applies.
|
|
92
|
-
const ms = node ? duration(node, '--dur-reveal') : 0;
|
|
93
|
-
const wait = delay ?? (node ? duration(node, '--skeleton-delay') : 0);
|
|
94
|
-
if (!node || ms <= 0 || elapsed < wait) {
|
|
95
|
-
phase = 'content';
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
|
-
const settle = getComputedStyle(node).getPropertyValue('--settle').trim();
|
|
99
|
-
timing = {
|
|
100
|
-
duration: ms,
|
|
101
|
-
easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out'
|
|
102
|
-
};
|
|
103
|
-
phase = 'reveal';
|
|
104
|
-
});
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
/** The solid colour behind `node`: the first background colour up the tree that is not transparent. */
|
|
108
|
-
function backdrop(node: Element): string {
|
|
109
|
-
for (let el: Element | null = node; el; el = el.parentElement) {
|
|
110
|
-
const colour = getComputedStyle(el).backgroundColor;
|
|
111
|
-
// Transparent is `rgba(…, 0)`, or `… / 0)` in the newer colour syntaxes.
|
|
112
|
-
if (colour !== 'transparent' && !/^rgba\(.*,\s*0\)$|\/\s*0\)$/.test(colour)) return colour;
|
|
113
|
-
}
|
|
114
|
-
// Nothing paints: the browser's canvas shows through.
|
|
115
|
-
return 'Canvas';
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// After the DOM update, in the same flush, so the content's first frame
|
|
119
|
-
// is already at opacity 0. The cleanup cancels the fade when a new load
|
|
120
|
-
// starts or the region goes away.
|
|
121
|
-
$effect(() => {
|
|
122
|
-
if (phase !== 'reveal') return;
|
|
123
|
-
return untrack(() => {
|
|
124
|
-
const node = ref;
|
|
125
|
-
const content = node?.querySelector<HTMLElement>(':scope > .hp-load-region-content');
|
|
126
|
-
if (!node || !content) {
|
|
127
|
-
phase = 'content';
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
content.style.backgroundColor = backdrop(node);
|
|
131
|
-
const fade = content.animate([{ opacity: 0 }, { opacity: 1 }], timing);
|
|
132
|
-
fade.onfinish = () => (phase = 'content');
|
|
133
|
-
return () => {
|
|
134
|
-
fade.cancel();
|
|
135
|
-
content.style.removeProperty('background-color');
|
|
136
|
-
};
|
|
137
|
-
});
|
|
138
|
-
});
|
|
139
|
-
</script>
|
|
140
|
-
|
|
141
|
-
<!--
|
|
142
|
-
A region that loads (BRIEF, motion rule 2): skeleton blocks while the
|
|
143
|
-
content loads, then the content. It owns the loading pattern of the
|
|
144
|
-
states docs: a visually hidden `role="status"` line before the region
|
|
145
|
-
that stays mounted, `aria-busy` on the region while `loading`, and the
|
|
146
|
-
`placeholder` in an `aria-hidden`, `inert` wrapper. When a load that
|
|
147
|
-
showed skeletons ends and `--dur-reveal` is more than 0ms, the content
|
|
148
|
-
crossfades in over the skeletons; see the script. Both layers are
|
|
149
|
-
`display: contents` at rest, so they add no box. During a reveal the
|
|
150
|
-
region is `position: relative`, the placeholder lies under the content
|
|
151
|
-
(`position: absolute; inset: 0`) and the content is a block on the
|
|
152
|
-
colour behind the region, so keep the region's own layout inside the
|
|
153
|
-
snippets and give their roots no outer margin. Focus is the page's job:
|
|
154
|
-
when the content replaces the focused control, the page moves focus to
|
|
155
|
-
`ref` or to the control needed next. Extra attributes and `class` go to
|
|
156
|
-
the region, `div.hp-load-region` (`tabindex="-1"`), which carries
|
|
157
|
-
`data-revealing` during a reveal.
|
|
158
|
-
-->
|
|
159
|
-
<p class="hp-visually-hidden" role="status">{status}</p>
|
|
160
|
-
<div
|
|
161
|
-
{...rest}
|
|
162
|
-
bind:this={ref}
|
|
163
|
-
class={['hp-load-region', className]}
|
|
164
|
-
tabindex="-1"
|
|
165
|
-
aria-busy={loading}
|
|
166
|
-
data-revealing={phase === 'reveal' ? '' : undefined}
|
|
167
|
-
>
|
|
168
|
-
{#key load}
|
|
169
|
-
{#if phase !== 'content'}
|
|
170
|
-
<div class="hp-load-region-placeholder" aria-hidden="true" inert>
|
|
171
|
-
{@render placeholder()}
|
|
172
|
-
</div>
|
|
173
|
-
{/if}
|
|
174
|
-
{/key}
|
|
175
|
-
{#if !loading}
|
|
176
|
-
<div class="hp-load-region-content">
|
|
177
|
-
{@render children()}
|
|
178
|
-
</div>
|
|
179
|
-
{/if}
|
|
180
|
-
</div>
|
|
181
|
-
|
|
182
|
-
<style>
|
|
183
|
-
/* Focus lands here only from the page's own code, when the content
|
|
184
|
-
replaces the focused control, as <main> takes it from the skip link:
|
|
185
|
-
no contour, since the region is not a control. */
|
|
186
|
-
.hp-load-region:focus {
|
|
187
|
-
outline: none;
|
|
188
|
-
}
|
|
189
|
-
/* At rest neither layer adds a box. */
|
|
190
|
-
.hp-load-region-placeholder,
|
|
191
|
-
.hp-load-region-content {
|
|
192
|
-
display: contents;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
/* ---- Reveal: the content over the placeholder ------------------------ */
|
|
196
|
-
|
|
197
|
-
.hp-load-region[data-revealing] {
|
|
198
|
-
position: relative;
|
|
199
|
-
}
|
|
200
|
-
/* Under the content, over the content's area. Its own stacking context
|
|
201
|
-
keeps a sticky table band (z-index) under the content too. Taller
|
|
202
|
-
placeholder rows are cut at the region's bottom edge; a band's bleed
|
|
203
|
-
into the gutter stays. */
|
|
204
|
-
.hp-load-region[data-revealing] > .hp-load-region-placeholder {
|
|
205
|
-
display: block;
|
|
206
|
-
position: absolute;
|
|
207
|
-
inset: 0;
|
|
208
|
-
isolation: isolate;
|
|
209
|
-
overflow-x: visible;
|
|
210
|
-
overflow-y: clip;
|
|
211
|
-
}
|
|
212
|
-
/* A box for the fade, painted after the placeholder. The script gives it
|
|
213
|
-
the colour behind the region, so it covers the placeholder as it fades
|
|
214
|
-
in. */
|
|
215
|
-
.hp-load-region[data-revealing] > .hp-load-region-content {
|
|
216
|
-
display: block;
|
|
217
|
-
position: relative;
|
|
218
|
-
}
|
|
219
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
export type LoadRegionProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'placeholder'> & {
|
|
6
|
+
/** True while the content loads: the region shows `placeholder` and is `aria-busy`. */
|
|
7
|
+
loading: boolean;
|
|
8
|
+
/**
|
|
9
|
+
* The text of the status line, such as `Loading tasks`, then `5 tasks`.
|
|
10
|
+
* The line is visually hidden, stays mounted outside the region and is
|
|
11
|
+
* a `role="status"` region, so each change is announced. Leave it empty
|
|
12
|
+
* while an error state announces itself.
|
|
13
|
+
*/
|
|
14
|
+
status: string;
|
|
15
|
+
/**
|
|
16
|
+
* The skeleton layout shown while loading, rendered inside an
|
|
17
|
+
* `aria-hidden`, `inert` wrapper. Give it the content's own layout
|
|
18
|
+
* container: during a reveal it lies under the content in a box of its
|
|
19
|
+
* own, so a grid or gap set on the region would not reach it.
|
|
20
|
+
*/
|
|
21
|
+
placeholder: Snippet;
|
|
22
|
+
/** The content, and also any empty or error state. */
|
|
23
|
+
children: Snippet;
|
|
24
|
+
/** The region element. A page moves focus here when the content replaces the focused control. */
|
|
25
|
+
ref?: HTMLDivElement | null;
|
|
26
|
+
};
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<script lang="ts">
|
|
30
|
+
import { untrack } from 'svelte';
|
|
31
|
+
import { duration } from '../motion/transitions.js';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
loading,
|
|
35
|
+
status,
|
|
36
|
+
placeholder,
|
|
37
|
+
children,
|
|
38
|
+
ref = $bindable(null),
|
|
39
|
+
class: className,
|
|
40
|
+
...rest
|
|
41
|
+
}: LoadRegionProps = $props();
|
|
42
|
+
|
|
43
|
+
// ---- Reveal (BRIEF, motion rule 2) -------------------------------------
|
|
44
|
+
|
|
45
|
+
// When a load ends after its skeletons became visible and --dur-reveal is
|
|
46
|
+
// more than 0ms, the content crossfades in over the skeletons for
|
|
47
|
+
// --dur-reveal on --settle. The placeholder stays mounted under the
|
|
48
|
+
// content, unchanged. The content lies on top on an opaque backdrop (the
|
|
49
|
+
// colour behind the region) and fades in from 0, so every pixel moves in
|
|
50
|
+
// a straight line from the placeholder's to the content's: a bar turns
|
|
51
|
+
// into its text, and a part drawn the same in both layers (a tag, a
|
|
52
|
+
// label, a table's band) does not change at all. Then the placeholder
|
|
53
|
+
// goes. At 0ms, for a load that ends within --skeleton-delay, on the
|
|
54
|
+
// first render and for any change that is not a load ending, the content
|
|
55
|
+
// replaces the placeholder in one frame. Opacity only: the skeletons
|
|
56
|
+
// mirror the layout, so nothing moves. Quiet motion, so it runs in both
|
|
57
|
+
// tiers; it is only a fade, so reduced motion keeps it.
|
|
58
|
+
|
|
59
|
+
type Phase = 'loading' | 'reveal' | 'content';
|
|
60
|
+
let phase = $state<Phase>(untrack(() => (loading ? 'loading' : 'content')));
|
|
61
|
+
// Counts loads. A new load remounts the placeholder, so its skeletons
|
|
62
|
+
// wait out --skeleton-delay again, also when it cuts a reveal short.
|
|
63
|
+
let load = $state(0);
|
|
64
|
+
|
|
65
|
+
/** When the running load began, or null between loads. */
|
|
66
|
+
let began: number | null = untrack(() => (loading ? performance.now() : null));
|
|
67
|
+
/** --skeleton-delay when the load began, as the skeletons read it; null until read. */
|
|
68
|
+
let delay: number | null = null;
|
|
69
|
+
/** The running reveal's timing, read from the region when the load ended. */
|
|
70
|
+
let timing: KeyframeAnimationOptions = {};
|
|
71
|
+
|
|
72
|
+
// Before the DOM update, so a reveal keeps the placeholder mounted (its
|
|
73
|
+
// skeletons stay visible) while the content mounts on top. Only `loading`
|
|
74
|
+
// is tracked.
|
|
75
|
+
$effect.pre(() => {
|
|
76
|
+
const busy = loading;
|
|
77
|
+
untrack(() => {
|
|
78
|
+
const node = ref;
|
|
79
|
+
if (busy) {
|
|
80
|
+
if (phase !== 'loading') {
|
|
81
|
+
began = performance.now();
|
|
82
|
+
delay = node ? duration(node, '--skeleton-delay') : null;
|
|
83
|
+
load += 1;
|
|
84
|
+
phase = 'loading';
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (phase !== 'loading') return;
|
|
89
|
+
const elapsed = began === null ? -1 : performance.now() - began;
|
|
90
|
+
began = null;
|
|
91
|
+
// Read now, so a token retuned during the load applies.
|
|
92
|
+
const ms = node ? duration(node, '--dur-reveal') : 0;
|
|
93
|
+
const wait = delay ?? (node ? duration(node, '--skeleton-delay') : 0);
|
|
94
|
+
if (!node || ms <= 0 || elapsed < wait) {
|
|
95
|
+
phase = 'content';
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const settle = getComputedStyle(node).getPropertyValue('--settle').trim();
|
|
99
|
+
timing = {
|
|
100
|
+
duration: ms,
|
|
101
|
+
easing: CSS.supports('transition-timing-function', settle) ? settle : 'ease-out'
|
|
102
|
+
};
|
|
103
|
+
phase = 'reveal';
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
/** The solid colour behind `node`: the first background colour up the tree that is not transparent. */
|
|
108
|
+
function backdrop(node: Element): string {
|
|
109
|
+
for (let el: Element | null = node; el; el = el.parentElement) {
|
|
110
|
+
const colour = getComputedStyle(el).backgroundColor;
|
|
111
|
+
// Transparent is `rgba(…, 0)`, or `… / 0)` in the newer colour syntaxes.
|
|
112
|
+
if (colour !== 'transparent' && !/^rgba\(.*,\s*0\)$|\/\s*0\)$/.test(colour)) return colour;
|
|
113
|
+
}
|
|
114
|
+
// Nothing paints: the browser's canvas shows through.
|
|
115
|
+
return 'Canvas';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// After the DOM update, in the same flush, so the content's first frame
|
|
119
|
+
// is already at opacity 0. The cleanup cancels the fade when a new load
|
|
120
|
+
// starts or the region goes away.
|
|
121
|
+
$effect(() => {
|
|
122
|
+
if (phase !== 'reveal') return;
|
|
123
|
+
return untrack(() => {
|
|
124
|
+
const node = ref;
|
|
125
|
+
const content = node?.querySelector<HTMLElement>(':scope > .hp-load-region-content');
|
|
126
|
+
if (!node || !content) {
|
|
127
|
+
phase = 'content';
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
content.style.backgroundColor = backdrop(node);
|
|
131
|
+
const fade = content.animate([{ opacity: 0 }, { opacity: 1 }], timing);
|
|
132
|
+
fade.onfinish = () => (phase = 'content');
|
|
133
|
+
return () => {
|
|
134
|
+
fade.cancel();
|
|
135
|
+
content.style.removeProperty('background-color');
|
|
136
|
+
};
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
</script>
|
|
140
|
+
|
|
141
|
+
<!--
|
|
142
|
+
A region that loads (BRIEF, motion rule 2): skeleton blocks while the
|
|
143
|
+
content loads, then the content. It owns the loading pattern of the
|
|
144
|
+
states docs: a visually hidden `role="status"` line before the region
|
|
145
|
+
that stays mounted, `aria-busy` on the region while `loading`, and the
|
|
146
|
+
`placeholder` in an `aria-hidden`, `inert` wrapper. When a load that
|
|
147
|
+
showed skeletons ends and `--dur-reveal` is more than 0ms, the content
|
|
148
|
+
crossfades in over the skeletons; see the script. Both layers are
|
|
149
|
+
`display: contents` at rest, so they add no box. During a reveal the
|
|
150
|
+
region is `position: relative`, the placeholder lies under the content
|
|
151
|
+
(`position: absolute; inset: 0`) and the content is a block on the
|
|
152
|
+
colour behind the region, so keep the region's own layout inside the
|
|
153
|
+
snippets and give their roots no outer margin. Focus is the page's job:
|
|
154
|
+
when the content replaces the focused control, the page moves focus to
|
|
155
|
+
`ref` or to the control needed next. Extra attributes and `class` go to
|
|
156
|
+
the region, `div.hp-load-region` (`tabindex="-1"`), which carries
|
|
157
|
+
`data-revealing` during a reveal.
|
|
158
|
+
-->
|
|
159
|
+
<p class="hp-visually-hidden" role="status">{status}</p>
|
|
160
|
+
<div
|
|
161
|
+
{...rest}
|
|
162
|
+
bind:this={ref}
|
|
163
|
+
class={['hp-load-region', className]}
|
|
164
|
+
tabindex="-1"
|
|
165
|
+
aria-busy={loading}
|
|
166
|
+
data-revealing={phase === 'reveal' ? '' : undefined}
|
|
167
|
+
>
|
|
168
|
+
{#key load}
|
|
169
|
+
{#if phase !== 'content'}
|
|
170
|
+
<div class="hp-load-region-placeholder" aria-hidden="true" inert>
|
|
171
|
+
{@render placeholder()}
|
|
172
|
+
</div>
|
|
173
|
+
{/if}
|
|
174
|
+
{/key}
|
|
175
|
+
{#if !loading}
|
|
176
|
+
<div class="hp-load-region-content">
|
|
177
|
+
{@render children()}
|
|
178
|
+
</div>
|
|
179
|
+
{/if}
|
|
180
|
+
</div>
|
|
181
|
+
|
|
182
|
+
<style>
|
|
183
|
+
/* Focus lands here only from the page's own code, when the content
|
|
184
|
+
replaces the focused control, as <main> takes it from the skip link:
|
|
185
|
+
no contour, since the region is not a control. */
|
|
186
|
+
.hp-load-region:focus {
|
|
187
|
+
outline: none;
|
|
188
|
+
}
|
|
189
|
+
/* At rest neither layer adds a box. */
|
|
190
|
+
.hp-load-region-placeholder,
|
|
191
|
+
.hp-load-region-content {
|
|
192
|
+
display: contents;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/* ---- Reveal: the content over the placeholder ------------------------ */
|
|
196
|
+
|
|
197
|
+
.hp-load-region[data-revealing] {
|
|
198
|
+
position: relative;
|
|
199
|
+
}
|
|
200
|
+
/* Under the content, over the content's area. Its own stacking context
|
|
201
|
+
keeps a sticky table band (z-index) under the content too. Taller
|
|
202
|
+
placeholder rows are cut at the region's bottom edge; a band's bleed
|
|
203
|
+
into the gutter stays. */
|
|
204
|
+
.hp-load-region[data-revealing] > .hp-load-region-placeholder {
|
|
205
|
+
display: block;
|
|
206
|
+
position: absolute;
|
|
207
|
+
inset: 0;
|
|
208
|
+
isolation: isolate;
|
|
209
|
+
overflow-x: visible;
|
|
210
|
+
overflow-y: clip;
|
|
211
|
+
}
|
|
212
|
+
/* A box for the fade, painted after the placeholder. The script gives it
|
|
213
|
+
the colour behind the region, so it covers the placeholder as it fades
|
|
214
|
+
in. */
|
|
215
|
+
.hp-load-region[data-revealing] > .hp-load-region-content {
|
|
216
|
+
display: block;
|
|
217
|
+
position: relative;
|
|
218
|
+
}
|
|
219
|
+
</style>
|
|
@@ -1,110 +1,110 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
|
|
5
|
-
export type MediaFrameProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
6
|
-
/**
|
|
7
|
-
* The frame's aspect ratio, any CSS `aspect-ratio` value such as
|
|
8
|
-
* `16 / 9`. Without it the frame takes its content's height, unless an
|
|
9
|
-
* ancestor sets `--media-frame-ratio` (a page sets that custom property
|
|
10
|
-
* in its media queries to change the ratio by breakpoint).
|
|
11
|
-
*/
|
|
12
|
-
ratio?: string;
|
|
13
|
-
/** The inner width in px, inside the 1px frame. Bindable, read-only. */
|
|
14
|
-
width?: number;
|
|
15
|
-
/** The inner height in px, inside the 1px frame. Bindable, read-only. */
|
|
16
|
-
height?: number;
|
|
17
|
-
/** The image, diagram or chart. */
|
|
18
|
-
children?: Snippet;
|
|
19
|
-
};
|
|
20
|
-
</script>
|
|
21
|
-
|
|
22
|
-
<script lang="ts">
|
|
23
|
-
let {
|
|
24
|
-
ratio,
|
|
25
|
-
width = $bindable(),
|
|
26
|
-
height = $bindable(),
|
|
27
|
-
class: className,
|
|
28
|
-
children,
|
|
29
|
-
...rest
|
|
30
|
-
}: MediaFrameProps = $props();
|
|
31
|
-
</script>
|
|
32
|
-
|
|
33
|
-
<!--
|
|
34
|
-
A 1px hairline frame with small + registration marks over its corners and
|
|
35
|
-
top centre (BRIEF, visual rule 8; da3-55, da3-61), for images and
|
|
36
|
-
diagrams, never text blocks. The marks paint over the content. Content
|
|
37
|
-
that fills the frame positions itself (absolute, inset 0) and reads
|
|
38
|
-
bind:width and bind:height to draw at the frame's real size.
|
|
39
|
-
-->
|
|
40
|
-
<div
|
|
41
|
-
{...rest}
|
|
42
|
-
class={['hp-media-frame', className]}
|
|
43
|
-
style:--media-frame-ratio={ratio}
|
|
44
|
-
bind:clientWidth={width}
|
|
45
|
-
bind:clientHeight={height}
|
|
46
|
-
>
|
|
47
|
-
{@render children?.()}
|
|
48
|
-
<span class="reg top left" aria-hidden="true"></span>
|
|
49
|
-
<span class="reg top mid" aria-hidden="true"></span>
|
|
50
|
-
<span class="reg top right" aria-hidden="true"></span>
|
|
51
|
-
<span class="reg bottom left" aria-hidden="true"></span>
|
|
52
|
-
<span class="reg bottom right" aria-hidden="true"></span>
|
|
53
|
-
</div>
|
|
54
|
-
|
|
55
|
-
<style>
|
|
56
|
-
.hp-media-frame {
|
|
57
|
-
position: relative;
|
|
58
|
-
aspect-ratio: var(--media-frame-ratio, auto);
|
|
59
|
-
border: 1px solid var(--border);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/* A 13px + whose 1px strokes cross at its centre pixel, 12px in from
|
|
63
|
-
the frame's inner edge. */
|
|
64
|
-
.reg {
|
|
65
|
-
position: absolute;
|
|
66
|
-
/* matches the approved specimen */
|
|
67
|
-
width: 13px;
|
|
68
|
-
/* matches the approved specimen */
|
|
69
|
-
height: 13px;
|
|
70
|
-
pointer-events: none;
|
|
71
|
-
}
|
|
72
|
-
.top {
|
|
73
|
-
top: 12px;
|
|
74
|
-
}
|
|
75
|
-
.bottom {
|
|
76
|
-
bottom: 12px;
|
|
77
|
-
}
|
|
78
|
-
.left {
|
|
79
|
-
left: 12px;
|
|
80
|
-
}
|
|
81
|
-
.right {
|
|
82
|
-
right: 12px;
|
|
83
|
-
}
|
|
84
|
-
.mid {
|
|
85
|
-
/* matches the approved specimen */
|
|
86
|
-
left: calc(50% - 6px);
|
|
87
|
-
}
|
|
88
|
-
.reg::before,
|
|
89
|
-
.reg::after {
|
|
90
|
-
content: '';
|
|
91
|
-
position: absolute;
|
|
92
|
-
background: var(--text);
|
|
93
|
-
}
|
|
94
|
-
.reg::before {
|
|
95
|
-
/* matches the approved specimen */
|
|
96
|
-
top: 6px;
|
|
97
|
-
left: 0;
|
|
98
|
-
/* matches the approved specimen */
|
|
99
|
-
width: 13px;
|
|
100
|
-
height: 1px;
|
|
101
|
-
}
|
|
102
|
-
.reg::after {
|
|
103
|
-
top: 0;
|
|
104
|
-
/* matches the approved specimen */
|
|
105
|
-
left: 6px;
|
|
106
|
-
width: 1px;
|
|
107
|
-
/* matches the approved specimen */
|
|
108
|
-
height: 13px;
|
|
109
|
-
}
|
|
110
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
export type MediaFrameProps = Omit<HTMLAttributes<HTMLDivElement>, 'children'> & {
|
|
6
|
+
/**
|
|
7
|
+
* The frame's aspect ratio, any CSS `aspect-ratio` value such as
|
|
8
|
+
* `16 / 9`. Without it the frame takes its content's height, unless an
|
|
9
|
+
* ancestor sets `--media-frame-ratio` (a page sets that custom property
|
|
10
|
+
* in its media queries to change the ratio by breakpoint).
|
|
11
|
+
*/
|
|
12
|
+
ratio?: string;
|
|
13
|
+
/** The inner width in px, inside the 1px frame. Bindable, read-only. */
|
|
14
|
+
width?: number;
|
|
15
|
+
/** The inner height in px, inside the 1px frame. Bindable, read-only. */
|
|
16
|
+
height?: number;
|
|
17
|
+
/** The image, diagram or chart. */
|
|
18
|
+
children?: Snippet;
|
|
19
|
+
};
|
|
20
|
+
</script>
|
|
21
|
+
|
|
22
|
+
<script lang="ts">
|
|
23
|
+
let {
|
|
24
|
+
ratio,
|
|
25
|
+
width = $bindable(),
|
|
26
|
+
height = $bindable(),
|
|
27
|
+
class: className,
|
|
28
|
+
children,
|
|
29
|
+
...rest
|
|
30
|
+
}: MediaFrameProps = $props();
|
|
31
|
+
</script>
|
|
32
|
+
|
|
33
|
+
<!--
|
|
34
|
+
A 1px hairline frame with small + registration marks over its corners and
|
|
35
|
+
top centre (BRIEF, visual rule 8; da3-55, da3-61), for images and
|
|
36
|
+
diagrams, never text blocks. The marks paint over the content. Content
|
|
37
|
+
that fills the frame positions itself (absolute, inset 0) and reads
|
|
38
|
+
bind:width and bind:height to draw at the frame's real size.
|
|
39
|
+
-->
|
|
40
|
+
<div
|
|
41
|
+
{...rest}
|
|
42
|
+
class={['hp-media-frame', className]}
|
|
43
|
+
style:--media-frame-ratio={ratio}
|
|
44
|
+
bind:clientWidth={width}
|
|
45
|
+
bind:clientHeight={height}
|
|
46
|
+
>
|
|
47
|
+
{@render children?.()}
|
|
48
|
+
<span class="reg top left" aria-hidden="true"></span>
|
|
49
|
+
<span class="reg top mid" aria-hidden="true"></span>
|
|
50
|
+
<span class="reg top right" aria-hidden="true"></span>
|
|
51
|
+
<span class="reg bottom left" aria-hidden="true"></span>
|
|
52
|
+
<span class="reg bottom right" aria-hidden="true"></span>
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
<style>
|
|
56
|
+
.hp-media-frame {
|
|
57
|
+
position: relative;
|
|
58
|
+
aspect-ratio: var(--media-frame-ratio, auto);
|
|
59
|
+
border: 1px solid var(--border);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/* A 13px + whose 1px strokes cross at its centre pixel, 12px in from
|
|
63
|
+
the frame's inner edge. */
|
|
64
|
+
.reg {
|
|
65
|
+
position: absolute;
|
|
66
|
+
/* matches the approved specimen */
|
|
67
|
+
width: 13px;
|
|
68
|
+
/* matches the approved specimen */
|
|
69
|
+
height: 13px;
|
|
70
|
+
pointer-events: none;
|
|
71
|
+
}
|
|
72
|
+
.top {
|
|
73
|
+
top: 12px;
|
|
74
|
+
}
|
|
75
|
+
.bottom {
|
|
76
|
+
bottom: 12px;
|
|
77
|
+
}
|
|
78
|
+
.left {
|
|
79
|
+
left: 12px;
|
|
80
|
+
}
|
|
81
|
+
.right {
|
|
82
|
+
right: 12px;
|
|
83
|
+
}
|
|
84
|
+
.mid {
|
|
85
|
+
/* matches the approved specimen */
|
|
86
|
+
left: calc(50% - 6px);
|
|
87
|
+
}
|
|
88
|
+
.reg::before,
|
|
89
|
+
.reg::after {
|
|
90
|
+
content: '';
|
|
91
|
+
position: absolute;
|
|
92
|
+
background: var(--text);
|
|
93
|
+
}
|
|
94
|
+
.reg::before {
|
|
95
|
+
/* matches the approved specimen */
|
|
96
|
+
top: 6px;
|
|
97
|
+
left: 0;
|
|
98
|
+
/* matches the approved specimen */
|
|
99
|
+
width: 13px;
|
|
100
|
+
height: 1px;
|
|
101
|
+
}
|
|
102
|
+
.reg::after {
|
|
103
|
+
top: 0;
|
|
104
|
+
/* matches the approved specimen */
|
|
105
|
+
left: 6px;
|
|
106
|
+
width: 1px;
|
|
107
|
+
/* matches the approved specimen */
|
|
108
|
+
height: 13px;
|
|
109
|
+
}
|
|
110
|
+
</style>
|