@mystravil/hardpoint 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +346 -0
  5. package/dist/components/AppShell.svelte +698 -0
  6. package/dist/components/AppShell.svelte.d.ts +77 -0
  7. package/dist/components/Button.svelte +174 -0
  8. package/dist/components/Button.svelte.d.ts +20 -0
  9. package/dist/components/Checkbox.svelte +198 -0
  10. package/dist/components/Checkbox.svelte.d.ts +16 -0
  11. package/dist/components/CheckboxGroup.svelte +99 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
  13. package/dist/components/Combobox.svelte +824 -0
  14. package/dist/components/Combobox.svelte.d.ts +63 -0
  15. package/dist/components/Dialog.svelte +356 -0
  16. package/dist/components/Dialog.svelte.d.ts +43 -0
  17. package/dist/components/Disclosure.svelte +201 -0
  18. package/dist/components/Disclosure.svelte.d.ts +27 -0
  19. package/dist/components/EmptyState.svelte +88 -0
  20. package/dist/components/EmptyState.svelte.d.ts +22 -0
  21. package/dist/components/ErrorState.svelte +135 -0
  22. package/dist/components/ErrorState.svelte.d.ts +26 -0
  23. package/dist/components/KeyValueList.svelte +114 -0
  24. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  25. package/dist/components/LoadRegion.svelte +219 -0
  26. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  27. package/dist/components/MediaFrame.svelte +110 -0
  28. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  29. package/dist/components/MediaImage.svelte +256 -0
  30. package/dist/components/MediaImage.svelte.d.ts +16 -0
  31. package/dist/components/Menu.svelte +258 -0
  32. package/dist/components/Menu.svelte.d.ts +36 -0
  33. package/dist/components/MenuCheckboxItem.svelte +119 -0
  34. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  35. package/dist/components/MenuGroup.svelte +50 -0
  36. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  37. package/dist/components/MenuItem.svelte +94 -0
  38. package/dist/components/MenuItem.svelte.d.ts +19 -0
  39. package/dist/components/MenuRadioGroup.svelte +62 -0
  40. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  41. package/dist/components/MenuRadioItem.svelte +110 -0
  42. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  43. package/dist/components/MenuSeparator.svelte +28 -0
  44. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  45. package/dist/components/NativeSelect.svelte +119 -0
  46. package/dist/components/NativeSelect.svelte.d.ts +46 -0
  47. package/dist/components/Pagination.svelte +132 -0
  48. package/dist/components/Pagination.svelte.d.ts +18 -0
  49. package/dist/components/Popover.svelte +222 -0
  50. package/dist/components/Popover.svelte.d.ts +35 -0
  51. package/dist/components/Progress.svelte +157 -0
  52. package/dist/components/Progress.svelte.d.ts +34 -0
  53. package/dist/components/Select.svelte +405 -0
  54. package/dist/components/Select.svelte.d.ts +42 -0
  55. package/dist/components/Skeleton.svelte +149 -0
  56. package/dist/components/Skeleton.svelte.d.ts +24 -0
  57. package/dist/components/Spinner.svelte +117 -0
  58. package/dist/components/Spinner.svelte.d.ts +13 -0
  59. package/dist/components/Stat.svelte +253 -0
  60. package/dist/components/Stat.svelte.d.ts +46 -0
  61. package/dist/components/Steps.svelte +189 -0
  62. package/dist/components/Steps.svelte.d.ts +31 -0
  63. package/dist/components/Switch.svelte +222 -0
  64. package/dist/components/Switch.svelte.d.ts +18 -0
  65. package/dist/components/Table.svelte +504 -0
  66. package/dist/components/Table.svelte.d.ts +92 -0
  67. package/dist/components/Tabs.svelte +246 -0
  68. package/dist/components/Tabs.svelte.d.ts +44 -0
  69. package/dist/components/Tag.svelte +40 -0
  70. package/dist/components/Tag.svelte.d.ts +15 -0
  71. package/dist/components/TextField.svelte +256 -0
  72. package/dist/components/TextField.svelte.d.ts +32 -0
  73. package/dist/components/Timeline.svelte +156 -0
  74. package/dist/components/Timeline.svelte.d.ts +30 -0
  75. package/dist/components/Toaster.svelte +411 -0
  76. package/dist/components/Toaster.svelte.d.ts +11 -0
  77. package/dist/components/Tooltip.svelte +200 -0
  78. package/dist/components/Tooltip.svelte.d.ts +38 -0
  79. package/dist/components/TooltipGroup.svelte +46 -0
  80. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  81. package/dist/components/internal/Scrim.svelte +52 -0
  82. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  83. package/dist/components/internal/Sheet.svelte +263 -0
  84. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  85. package/dist/components/internal/fade-in.d.ts +10 -0
  86. package/dist/components/internal/fade-in.js +20 -0
  87. package/dist/components/internal/resolve.d.ts +8 -0
  88. package/dist/components/internal/resolve.js +247 -0
  89. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  90. package/dist/components/internal/swipe-dismiss.js +220 -0
  91. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  92. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  93. package/dist/components/toast.svelte.d.ts +57 -0
  94. package/dist/components/toast.svelte.js +84 -0
  95. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  96. package/dist/fonts/OFL.txt +93 -0
  97. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  98. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  99. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  100. package/dist/fonts.css +71 -0
  101. package/dist/index.d.ts +39 -0
  102. package/dist/index.js +39 -0
  103. package/dist/motion/Motion.svelte +15 -0
  104. package/dist/motion/Motion.svelte.d.ts +9 -0
  105. package/dist/motion/context.d.ts +18 -0
  106. package/dist/motion/context.js +26 -0
  107. package/dist/motion/index.d.ts +3 -0
  108. package/dist/motion/index.js +3 -0
  109. package/dist/motion/transitions.d.ts +31 -0
  110. package/dist/motion/transitions.js +112 -0
  111. package/dist/tokens.css +279 -0
  112. package/package.json +76 -6
@@ -0,0 +1,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;