@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.
- package/CHANGELOG.md +18 -0
- package/LICENSE +25 -0
- package/README.md +64 -3
- package/dist/base.css +346 -0
- package/dist/components/AppShell.svelte +698 -0
- package/dist/components/AppShell.svelte.d.ts +77 -0
- package/dist/components/Button.svelte +174 -0
- package/dist/components/Button.svelte.d.ts +20 -0
- package/dist/components/Checkbox.svelte +198 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/CheckboxGroup.svelte +99 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
- package/dist/components/Combobox.svelte +824 -0
- package/dist/components/Combobox.svelte.d.ts +63 -0
- package/dist/components/Dialog.svelte +356 -0
- package/dist/components/Dialog.svelte.d.ts +43 -0
- package/dist/components/Disclosure.svelte +201 -0
- package/dist/components/Disclosure.svelte.d.ts +27 -0
- package/dist/components/EmptyState.svelte +88 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/ErrorState.svelte +135 -0
- package/dist/components/ErrorState.svelte.d.ts +26 -0
- package/dist/components/KeyValueList.svelte +114 -0
- package/dist/components/KeyValueList.svelte.d.ts +43 -0
- package/dist/components/LoadRegion.svelte +219 -0
- package/dist/components/LoadRegion.svelte.d.ts +27 -0
- package/dist/components/MediaFrame.svelte +110 -0
- package/dist/components/MediaFrame.svelte.d.ts +20 -0
- package/dist/components/MediaImage.svelte +256 -0
- package/dist/components/MediaImage.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +258 -0
- package/dist/components/Menu.svelte.d.ts +36 -0
- package/dist/components/MenuCheckboxItem.svelte +119 -0
- package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
- package/dist/components/MenuGroup.svelte +50 -0
- package/dist/components/MenuGroup.svelte.d.ts +15 -0
- package/dist/components/MenuItem.svelte +94 -0
- package/dist/components/MenuItem.svelte.d.ts +19 -0
- package/dist/components/MenuRadioGroup.svelte +62 -0
- package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/components/MenuRadioItem.svelte +110 -0
- package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
- package/dist/components/MenuSeparator.svelte +28 -0
- package/dist/components/MenuSeparator.svelte.d.ts +6 -0
- package/dist/components/NativeSelect.svelte +119 -0
- package/dist/components/NativeSelect.svelte.d.ts +46 -0
- package/dist/components/Pagination.svelte +132 -0
- package/dist/components/Pagination.svelte.d.ts +18 -0
- package/dist/components/Popover.svelte +222 -0
- package/dist/components/Popover.svelte.d.ts +35 -0
- package/dist/components/Progress.svelte +157 -0
- package/dist/components/Progress.svelte.d.ts +34 -0
- package/dist/components/Select.svelte +405 -0
- package/dist/components/Select.svelte.d.ts +42 -0
- package/dist/components/Skeleton.svelte +149 -0
- package/dist/components/Skeleton.svelte.d.ts +24 -0
- package/dist/components/Spinner.svelte +117 -0
- package/dist/components/Spinner.svelte.d.ts +13 -0
- package/dist/components/Stat.svelte +253 -0
- package/dist/components/Stat.svelte.d.ts +46 -0
- package/dist/components/Steps.svelte +189 -0
- package/dist/components/Steps.svelte.d.ts +31 -0
- package/dist/components/Switch.svelte +222 -0
- package/dist/components/Switch.svelte.d.ts +18 -0
- package/dist/components/Table.svelte +504 -0
- package/dist/components/Table.svelte.d.ts +92 -0
- package/dist/components/Tabs.svelte +246 -0
- package/dist/components/Tabs.svelte.d.ts +44 -0
- package/dist/components/Tag.svelte +40 -0
- package/dist/components/Tag.svelte.d.ts +15 -0
- package/dist/components/TextField.svelte +256 -0
- package/dist/components/TextField.svelte.d.ts +32 -0
- package/dist/components/Timeline.svelte +156 -0
- package/dist/components/Timeline.svelte.d.ts +30 -0
- package/dist/components/Toaster.svelte +411 -0
- package/dist/components/Toaster.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +200 -0
- package/dist/components/Tooltip.svelte.d.ts +38 -0
- package/dist/components/TooltipGroup.svelte +46 -0
- package/dist/components/TooltipGroup.svelte.d.ts +29 -0
- package/dist/components/internal/Scrim.svelte +52 -0
- package/dist/components/internal/Scrim.svelte.d.ts +15 -0
- package/dist/components/internal/Sheet.svelte +263 -0
- package/dist/components/internal/Sheet.svelte.d.ts +36 -0
- package/dist/components/internal/fade-in.d.ts +10 -0
- package/dist/components/internal/fade-in.js +20 -0
- package/dist/components/internal/resolve.d.ts +8 -0
- package/dist/components/internal/resolve.js +247 -0
- package/dist/components/internal/swipe-dismiss.d.ts +47 -0
- package/dist/components/internal/swipe-dismiss.js +220 -0
- package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
- package/dist/components/internal/visual-viewport.svelte.js +79 -0
- package/dist/components/toast.svelte.d.ts +57 -0
- package/dist/components/toast.svelte.js +84 -0
- package/dist/fonts/OFL-geist-pixel.txt +92 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/fonts/geist-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-pixel-square.woff2 +0 -0
- package/dist/fonts.css +71 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +39 -0
- package/dist/motion/Motion.svelte +15 -0
- package/dist/motion/Motion.svelte.d.ts +9 -0
- package/dist/motion/context.d.ts +18 -0
- package/dist/motion/context.js +26 -0
- package/dist/motion/index.d.ts +3 -0
- package/dist/motion/index.js +3 -0
- package/dist/motion/transitions.d.ts +31 -0
- package/dist/motion/transitions.js +112 -0
- package/dist/tokens.css +279 -0
- package/package.json +76 -6
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `standard` (default): the title at the section-title size. `display`:
|
|
7
|
+
* the title in Geist Pixel Square, as the screen's one display line.
|
|
8
|
+
*/
|
|
9
|
+
export type EmptyStateVariant = 'standard' | 'display';
|
|
10
|
+
|
|
11
|
+
/** The title's element: `p` by default, a heading where the state names a region of the page. */
|
|
12
|
+
export type EmptyStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
13
|
+
|
|
14
|
+
export type EmptyStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
|
|
15
|
+
/** What is empty, as a short fixed line: `No projects yet`. Never transformed. */
|
|
16
|
+
title: string;
|
|
17
|
+
/** Why it is empty or what fills it, in the muted ink. */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** The one clear next action: render a `Button` here. */
|
|
20
|
+
action?: Snippet;
|
|
21
|
+
titleAs?: EmptyStateTitleElement;
|
|
22
|
+
variant?: EmptyStateVariant;
|
|
23
|
+
};
|
|
24
|
+
</script>
|
|
25
|
+
|
|
26
|
+
<script lang="ts">
|
|
27
|
+
let {
|
|
28
|
+
title,
|
|
29
|
+
description,
|
|
30
|
+
action,
|
|
31
|
+
titleAs = 'p',
|
|
32
|
+
variant = 'standard',
|
|
33
|
+
class: className,
|
|
34
|
+
...rest
|
|
35
|
+
}: EmptyStateProps = $props();
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<!--
|
|
39
|
+
A data view with nothing to show (BRIEF, Apps and phones: States): a title,
|
|
40
|
+
an optional description and one clear next action, aligned to the text
|
|
41
|
+
edge with no box around them. It sets no outer margin; the page or the
|
|
42
|
+
Table's `empty` snippet places it. Extra attributes and `class` go to the
|
|
43
|
+
root `div.hp-empty-state`.
|
|
44
|
+
-->
|
|
45
|
+
<div {...rest} class={['hp-empty-state', className]} data-variant={variant}>
|
|
46
|
+
<svelte:element
|
|
47
|
+
this={titleAs}
|
|
48
|
+
class={['hp-empty-state-title', variant === 'display' ? 'hp-pixel' : 'hp-section-title']}
|
|
49
|
+
>{title}</svelte:element
|
|
50
|
+
>
|
|
51
|
+
{#if description}
|
|
52
|
+
<p class="hp-empty-state-description">{description}</p>
|
|
53
|
+
{/if}
|
|
54
|
+
{#if action}
|
|
55
|
+
<div class="hp-empty-state-action">{@render action()}</div>
|
|
56
|
+
{/if}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<style>
|
|
60
|
+
.hp-empty-state {
|
|
61
|
+
display: grid;
|
|
62
|
+
justify-items: start;
|
|
63
|
+
min-width: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.hp-empty-state-title {
|
|
67
|
+
max-width: 100%;
|
|
68
|
+
overflow-wrap: anywhere;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/* Reading text: 16px at every width, in the reading measure. */
|
|
72
|
+
.hp-empty-state-description {
|
|
73
|
+
max-width: var(--measure);
|
|
74
|
+
margin-top: var(--space-2);
|
|
75
|
+
color: var(--muted);
|
|
76
|
+
font-size: var(--type-body);
|
|
77
|
+
line-height: 24px;
|
|
78
|
+
overflow-wrap: anywhere;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/* The action is its own group: three times the gap inside the text. */
|
|
82
|
+
.hp-empty-state-action {
|
|
83
|
+
display: flex;
|
|
84
|
+
flex-wrap: wrap;
|
|
85
|
+
gap: var(--space-2);
|
|
86
|
+
margin-top: var(--space-6);
|
|
87
|
+
}
|
|
88
|
+
</style>
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* `standard` (default): the title at the section-title size. `display`:
|
|
5
|
+
* the title in Geist Pixel Square, as the screen's one display line.
|
|
6
|
+
*/
|
|
7
|
+
export type EmptyStateVariant = 'standard' | 'display';
|
|
8
|
+
/** The title's element: `p` by default, a heading where the state names a region of the page. */
|
|
9
|
+
export type EmptyStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
10
|
+
export type EmptyStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
|
|
11
|
+
/** What is empty, as a short fixed line: `No projects yet`. Never transformed. */
|
|
12
|
+
title: string;
|
|
13
|
+
/** Why it is empty or what fills it, in the muted ink. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** The one clear next action: render a `Button` here. */
|
|
16
|
+
action?: Snippet;
|
|
17
|
+
titleAs?: EmptyStateTitleElement;
|
|
18
|
+
variant?: EmptyStateVariant;
|
|
19
|
+
};
|
|
20
|
+
declare const EmptyState: import("svelte").Component<EmptyStateProps, {}, "">;
|
|
21
|
+
type EmptyState = ReturnType<typeof EmptyState>;
|
|
22
|
+
export default EmptyState;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* `standard` (default): the title at the section-title size. `display`:
|
|
7
|
+
* the title in Geist Pixel Square, as the screen's one display line.
|
|
8
|
+
*/
|
|
9
|
+
export type ErrorStateVariant = 'standard' | 'display';
|
|
10
|
+
|
|
11
|
+
/** The title's element: `p` by default, a heading where the state names a region of the page. */
|
|
12
|
+
export type ErrorStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
13
|
+
|
|
14
|
+
export type ErrorStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
|
|
15
|
+
/** What failed, as a short line: `Couldn't load projects`. Never transformed. */
|
|
16
|
+
title: string;
|
|
17
|
+
/** How to fix it: `Check your connection, then try again.` Errors never apologise. */
|
|
18
|
+
description?: string;
|
|
19
|
+
/** Shows a retry button that calls this. */
|
|
20
|
+
onretry?: () => void;
|
|
21
|
+
/** The retry button's label. Defaults to `Try again`. */
|
|
22
|
+
retryLabel?: string;
|
|
23
|
+
/** A fix that is not a retry, such as signing in again: render a `Button` here. It follows the retry button. */
|
|
24
|
+
action?: Snippet;
|
|
25
|
+
titleAs?: ErrorStateTitleElement;
|
|
26
|
+
variant?: ErrorStateVariant;
|
|
27
|
+
};
|
|
28
|
+
</script>
|
|
29
|
+
|
|
30
|
+
<script lang="ts">
|
|
31
|
+
import Button from './Button.svelte';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
title,
|
|
35
|
+
description,
|
|
36
|
+
onretry,
|
|
37
|
+
retryLabel = 'Try again',
|
|
38
|
+
action,
|
|
39
|
+
titleAs = 'p',
|
|
40
|
+
variant = 'standard',
|
|
41
|
+
class: className,
|
|
42
|
+
...rest
|
|
43
|
+
}: ErrorStateProps = $props();
|
|
44
|
+
</script>
|
|
45
|
+
|
|
46
|
+
<!--
|
|
47
|
+
A data view that failed to load, shown where its content would be (BRIEF,
|
|
48
|
+
Apps and phones: States): what failed, how to fix it, and a retry action.
|
|
49
|
+
The title carries an outlined square with a "!" in the status red, so the
|
|
50
|
+
state never rests on colour alone; the text stays in the two inks. The
|
|
51
|
+
title and description sit in a role="alert" region, so a screen reader
|
|
52
|
+
announces the failure when the state replaces the content; the actions
|
|
53
|
+
stay outside it. It sets no outer margin. Extra attributes and `class` go
|
|
54
|
+
to the root `div.hp-error-state`.
|
|
55
|
+
-->
|
|
56
|
+
<div {...rest} class={['hp-error-state', className]} data-variant={variant}>
|
|
57
|
+
<div class="hp-error-state-message" role="alert">
|
|
58
|
+
<svelte:element
|
|
59
|
+
this={titleAs}
|
|
60
|
+
class={['hp-error-state-title', variant === 'display' ? 'hp-pixel' : 'hp-section-title']}
|
|
61
|
+
>
|
|
62
|
+
<span class="hp-error-state-icon" aria-hidden="true">
|
|
63
|
+
<svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
|
|
64
|
+
<path d="M1.5 1.5h13v13h-13z" />
|
|
65
|
+
<path d="M8 4v4M8 10v2" stroke-width="2" />
|
|
66
|
+
</svg>
|
|
67
|
+
</span>
|
|
68
|
+
<span>{title}</span>
|
|
69
|
+
</svelte:element>
|
|
70
|
+
{#if description}
|
|
71
|
+
<p class="hp-error-state-description">{description}</p>
|
|
72
|
+
{/if}
|
|
73
|
+
</div>
|
|
74
|
+
{#if onretry || action}
|
|
75
|
+
<div class="hp-error-state-actions">
|
|
76
|
+
{#if onretry}
|
|
77
|
+
<!-- type="button", so a retry inside a form never submits it. -->
|
|
78
|
+
<Button type="button" onclick={onretry}>{retryLabel}</Button>
|
|
79
|
+
{/if}
|
|
80
|
+
{@render action?.()}
|
|
81
|
+
</div>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<style>
|
|
86
|
+
.hp-error-state {
|
|
87
|
+
display: grid;
|
|
88
|
+
justify-items: start;
|
|
89
|
+
min-width: 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.hp-error-state-message {
|
|
93
|
+
display: grid;
|
|
94
|
+
justify-items: start;
|
|
95
|
+
gap: var(--space-2);
|
|
96
|
+
max-width: 100%;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/* The icon hangs before the title's first line, as on a field error. */
|
|
100
|
+
.hp-error-state-title {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: var(--space-2);
|
|
103
|
+
max-width: 100%;
|
|
104
|
+
overflow-wrap: anywhere;
|
|
105
|
+
}
|
|
106
|
+
/* One line box high, so the icon centres on the first line of the title
|
|
107
|
+
however it wraps. */
|
|
108
|
+
.hp-error-state-icon {
|
|
109
|
+
display: flex;
|
|
110
|
+
flex: none;
|
|
111
|
+
align-items: center;
|
|
112
|
+
height: 1lh;
|
|
113
|
+
color: var(--danger);
|
|
114
|
+
}
|
|
115
|
+
.hp-error-state-icon svg {
|
|
116
|
+
display: block;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* Reading text: 16px at every width, in the reading measure. */
|
|
120
|
+
.hp-error-state-description {
|
|
121
|
+
max-width: var(--measure);
|
|
122
|
+
color: var(--muted);
|
|
123
|
+
font-size: var(--type-body);
|
|
124
|
+
line-height: 24px;
|
|
125
|
+
overflow-wrap: anywhere;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* The actions are their own group: three times the gap inside the text. */
|
|
129
|
+
.hp-error-state-actions {
|
|
130
|
+
display: flex;
|
|
131
|
+
flex-wrap: wrap;
|
|
132
|
+
gap: var(--space-2);
|
|
133
|
+
margin-top: var(--space-6);
|
|
134
|
+
}
|
|
135
|
+
</style>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
/**
|
|
4
|
+
* `standard` (default): the title at the section-title size. `display`:
|
|
5
|
+
* the title in Geist Pixel Square, as the screen's one display line.
|
|
6
|
+
*/
|
|
7
|
+
export type ErrorStateVariant = 'standard' | 'display';
|
|
8
|
+
/** The title's element: `p` by default, a heading where the state names a region of the page. */
|
|
9
|
+
export type ErrorStateTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
|
|
10
|
+
export type ErrorStateProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
|
|
11
|
+
/** What failed, as a short line: `Couldn't load projects`. Never transformed. */
|
|
12
|
+
title: string;
|
|
13
|
+
/** How to fix it: `Check your connection, then try again.` Errors never apologise. */
|
|
14
|
+
description?: string;
|
|
15
|
+
/** Shows a retry button that calls this. */
|
|
16
|
+
onretry?: () => void;
|
|
17
|
+
/** The retry button's label. Defaults to `Try again`. */
|
|
18
|
+
retryLabel?: string;
|
|
19
|
+
/** A fix that is not a retry, such as signing in again: render a `Button` here. It follows the retry button. */
|
|
20
|
+
action?: Snippet;
|
|
21
|
+
titleAs?: ErrorStateTitleElement;
|
|
22
|
+
variant?: ErrorStateVariant;
|
|
23
|
+
};
|
|
24
|
+
declare const ErrorState: import("svelte").Component<ErrorStateProps, {}, "">;
|
|
25
|
+
type ErrorState = ReturnType<typeof ErrorState>;
|
|
26
|
+
export default ErrorState;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
|
|
5
|
+
export type KeyValueItem = {
|
|
6
|
+
/** The caps label in the first column. A fixed string the app defines. */
|
|
7
|
+
key: string;
|
|
8
|
+
/** The value in Geist Mono. */
|
|
9
|
+
value: string | number;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/** Items may carry extra fields (an `href`, a status) that the `value` snippet reads. */
|
|
13
|
+
export type KeyValueListProps<T extends KeyValueItem = KeyValueItem> = Omit<
|
|
14
|
+
HTMLAttributes<HTMLDListElement>,
|
|
15
|
+
'children'
|
|
16
|
+
> & {
|
|
17
|
+
items: T[];
|
|
18
|
+
/**
|
|
19
|
+
* The label column's width from 600px up: any CSS width. Defaults to
|
|
20
|
+
* `9rem`. Below 600px the label stacks over the value.
|
|
21
|
+
*/
|
|
22
|
+
keyWidth?: string;
|
|
23
|
+
/** Renders one value, for a link or a status. Default: the text. */
|
|
24
|
+
value?: Snippet<[T]>;
|
|
25
|
+
};
|
|
26
|
+
</script>
|
|
27
|
+
|
|
28
|
+
<script lang="ts" generics="T extends KeyValueItem = KeyValueItem">
|
|
29
|
+
import { onMount, untrack } from 'svelte';
|
|
30
|
+
import { useMotion } from '../motion/index.js';
|
|
31
|
+
|
|
32
|
+
let { items, keyWidth, value, class: className, ...rest }: KeyValueListProps<T> = $props();
|
|
33
|
+
|
|
34
|
+
// Expressive stagger (motion rule 7), as in Table: only the items of the
|
|
35
|
+
// first render enter, by their index. Any change to `items` (a new array
|
|
36
|
+
// or a changed length) ends the entrance, and so does its last animation.
|
|
37
|
+
const motion = useMotion();
|
|
38
|
+
const firstItems = untrack(() => items);
|
|
39
|
+
const firstCount = untrack(() => items.length);
|
|
40
|
+
let entering = $state.raw(motion.expressive);
|
|
41
|
+
let root: HTMLDListElement;
|
|
42
|
+
|
|
43
|
+
$effect.pre(() => {
|
|
44
|
+
if (items !== firstItems || items.length !== firstCount) entering = false;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
onMount(() => {
|
|
48
|
+
if (!entering) return;
|
|
49
|
+
const running = root
|
|
50
|
+
.getAnimations({ subtree: true })
|
|
51
|
+
.filter((a) => a instanceof CSSAnimation && a.animationName === 'hp-enter');
|
|
52
|
+
Promise.allSettled(running.map((a) => a.finished)).then(() => {
|
|
53
|
+
entering = false;
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
</script>
|
|
57
|
+
|
|
58
|
+
<!--
|
|
59
|
+
A caps label column and a Geist Mono value column, without separators
|
|
60
|
+
(BRIEF, visual rule 6; da3-53). Compact widths stack each label over its
|
|
61
|
+
value. Extra attributes and `class` go to the <dl>.
|
|
62
|
+
-->
|
|
63
|
+
<dl
|
|
64
|
+
{...rest}
|
|
65
|
+
bind:this={root}
|
|
66
|
+
class={['hp-key-value-list', className]}
|
|
67
|
+
style:--key-value-list-key-width={keyWidth}
|
|
68
|
+
>
|
|
69
|
+
{#each items as item, i}
|
|
70
|
+
<div class={[entering && 'hp-enter']} style:--i={entering ? i : undefined}>
|
|
71
|
+
<dt class="hp-label">{item.key}</dt>
|
|
72
|
+
<dd>{#if value}{@render value(item)}{:else}{item.value}{/if}</dd>
|
|
73
|
+
</div>
|
|
74
|
+
{/each}
|
|
75
|
+
</dl>
|
|
76
|
+
|
|
77
|
+
<style>
|
|
78
|
+
.hp-key-value-list {
|
|
79
|
+
display: grid;
|
|
80
|
+
gap: var(--space-3);
|
|
81
|
+
}
|
|
82
|
+
div {
|
|
83
|
+
display: grid;
|
|
84
|
+
/* matches the approved specimen */
|
|
85
|
+
gap: 2px;
|
|
86
|
+
}
|
|
87
|
+
dt {
|
|
88
|
+
color: var(--muted);
|
|
89
|
+
}
|
|
90
|
+
/* 16px on touch, the dense 14px on fine pointers. A long identifier or
|
|
91
|
+
path breaks anywhere rather than widen the page. */
|
|
92
|
+
dd {
|
|
93
|
+
font-family: var(--mono);
|
|
94
|
+
font-size: var(--type-body);
|
|
95
|
+
line-height: 24px;
|
|
96
|
+
overflow-wrap: anywhere;
|
|
97
|
+
}
|
|
98
|
+
@media (pointer: fine) {
|
|
99
|
+
dd {
|
|
100
|
+
font-size: var(--type-dense);
|
|
101
|
+
line-height: 20px;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
@media (min-width: 600px) {
|
|
105
|
+
.hp-key-value-list {
|
|
106
|
+
gap: var(--space-2);
|
|
107
|
+
}
|
|
108
|
+
div {
|
|
109
|
+
grid-template-columns: var(--key-value-list-key-width, 9rem) minmax(0, 1fr);
|
|
110
|
+
align-items: baseline;
|
|
111
|
+
gap: var(--space-4);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
</style>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
export type KeyValueItem = {
|
|
4
|
+
/** The caps label in the first column. A fixed string the app defines. */
|
|
5
|
+
key: string;
|
|
6
|
+
/** The value in Geist Mono. */
|
|
7
|
+
value: string | number;
|
|
8
|
+
};
|
|
9
|
+
/** Items may carry extra fields (an `href`, a status) that the `value` snippet reads. */
|
|
10
|
+
export type KeyValueListProps<T extends KeyValueItem = KeyValueItem> = Omit<HTMLAttributes<HTMLDListElement>, 'children'> & {
|
|
11
|
+
items: T[];
|
|
12
|
+
/**
|
|
13
|
+
* The label column's width from 600px up: any CSS width. Defaults to
|
|
14
|
+
* `9rem`. Below 600px the label stacks over the value.
|
|
15
|
+
*/
|
|
16
|
+
keyWidth?: string;
|
|
17
|
+
/** Renders one value, for a link or a status. Default: the text. */
|
|
18
|
+
value?: Snippet<[T]>;
|
|
19
|
+
};
|
|
20
|
+
declare function $$render<T extends KeyValueItem = KeyValueItem>(): {
|
|
21
|
+
props: KeyValueListProps<T>;
|
|
22
|
+
exports: {};
|
|
23
|
+
bindings: "";
|
|
24
|
+
slots: {};
|
|
25
|
+
events: {};
|
|
26
|
+
};
|
|
27
|
+
declare class __sveltets_Render<T extends KeyValueItem = KeyValueItem> {
|
|
28
|
+
props(): ReturnType<typeof $$render<T>>['props'];
|
|
29
|
+
events(): ReturnType<typeof $$render<T>>['events'];
|
|
30
|
+
slots(): ReturnType<typeof $$render<T>>['slots'];
|
|
31
|
+
bindings(): "";
|
|
32
|
+
exports(): {};
|
|
33
|
+
}
|
|
34
|
+
interface $$IsomorphicComponent {
|
|
35
|
+
new <T extends KeyValueItem = KeyValueItem>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
|
|
36
|
+
$$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
|
|
37
|
+
} & ReturnType<__sveltets_Render<T>['exports']>;
|
|
38
|
+
<T extends KeyValueItem = KeyValueItem>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
|
|
39
|
+
z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
|
|
40
|
+
}
|
|
41
|
+
declare const KeyValueList: $$IsomorphicComponent;
|
|
42
|
+
type KeyValueList<T extends KeyValueItem = KeyValueItem> = InstanceType<typeof KeyValueList<T>>;
|
|
43
|
+
export default KeyValueList;
|
|
@@ -0,0 +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>
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
export type LoadRegionProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'placeholder'> & {
|
|
4
|
+
/** True while the content loads: the region shows `placeholder` and is `aria-busy`. */
|
|
5
|
+
loading: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The text of the status line, such as `Loading tasks`, then `5 tasks`.
|
|
8
|
+
* The line is visually hidden, stays mounted outside the region and is
|
|
9
|
+
* a `role="status"` region, so each change is announced. Leave it empty
|
|
10
|
+
* while an error state announces itself.
|
|
11
|
+
*/
|
|
12
|
+
status: string;
|
|
13
|
+
/**
|
|
14
|
+
* The skeleton layout shown while loading, rendered inside an
|
|
15
|
+
* `aria-hidden`, `inert` wrapper. Give it the content's own layout
|
|
16
|
+
* container: during a reveal it lies under the content in a box of its
|
|
17
|
+
* own, so a grid or gap set on the region would not reach it.
|
|
18
|
+
*/
|
|
19
|
+
placeholder: Snippet;
|
|
20
|
+
/** The content, and also any empty or error state. */
|
|
21
|
+
children: Snippet;
|
|
22
|
+
/** The region element. A page moves focus here when the content replaces the focused control. */
|
|
23
|
+
ref?: HTMLDivElement | null;
|
|
24
|
+
};
|
|
25
|
+
declare const LoadRegion: import("svelte").Component<LoadRegionProps, {}, "ref">;
|
|
26
|
+
type LoadRegion = ReturnType<typeof LoadRegion>;
|
|
27
|
+
export default LoadRegion;
|