@mystravil/hardpoint 0.0.0-stage → 0.2.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 (126) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +625 -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 +235 -0
  10. package/dist/components/Checkbox.svelte.d.ts +23 -0
  11. package/dist/components/CheckboxGroup.svelte +207 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +44 -0
  13. package/dist/components/Combobox.svelte +901 -0
  14. package/dist/components/Combobox.svelte.d.ts +72 -0
  15. package/dist/components/Dialog.svelte +359 -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/ImageViewer.svelte +764 -0
  24. package/dist/components/ImageViewer.svelte.d.ts +55 -0
  25. package/dist/components/KeyValueList.svelte +114 -0
  26. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  27. package/dist/components/ListDetail.svelte +218 -0
  28. package/dist/components/ListDetail.svelte.d.ts +37 -0
  29. package/dist/components/LoadRegion.svelte +219 -0
  30. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  31. package/dist/components/MediaFrame.svelte +110 -0
  32. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  33. package/dist/components/MediaImage.svelte +256 -0
  34. package/dist/components/MediaImage.svelte.d.ts +16 -0
  35. package/dist/components/Menu.svelte +258 -0
  36. package/dist/components/Menu.svelte.d.ts +36 -0
  37. package/dist/components/MenuCheckboxItem.svelte +119 -0
  38. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  39. package/dist/components/MenuGroup.svelte +50 -0
  40. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  41. package/dist/components/MenuItem.svelte +94 -0
  42. package/dist/components/MenuItem.svelte.d.ts +19 -0
  43. package/dist/components/MenuRadioGroup.svelte +62 -0
  44. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  45. package/dist/components/MenuRadioItem.svelte +110 -0
  46. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  47. package/dist/components/MenuSeparator.svelte +28 -0
  48. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  49. package/dist/components/Meter.svelte +283 -0
  50. package/dist/components/Meter.svelte.d.ts +66 -0
  51. package/dist/components/NativeSelect.svelte +135 -0
  52. package/dist/components/NativeSelect.svelte.d.ts +52 -0
  53. package/dist/components/Notice.svelte +262 -0
  54. package/dist/components/Notice.svelte.d.ts +30 -0
  55. package/dist/components/Pagination.svelte +132 -0
  56. package/dist/components/Pagination.svelte.d.ts +18 -0
  57. package/dist/components/Popover.svelte +222 -0
  58. package/dist/components/Popover.svelte.d.ts +35 -0
  59. package/dist/components/Progress.svelte +157 -0
  60. package/dist/components/Progress.svelte.d.ts +34 -0
  61. package/dist/components/RadioGroup.svelte +440 -0
  62. package/dist/components/RadioGroup.svelte.d.ts +50 -0
  63. package/dist/components/Select.svelte +471 -0
  64. package/dist/components/Select.svelte.d.ts +48 -0
  65. package/dist/components/Skeleton.svelte +149 -0
  66. package/dist/components/Skeleton.svelte.d.ts +24 -0
  67. package/dist/components/Spinner.svelte +118 -0
  68. package/dist/components/Spinner.svelte.d.ts +13 -0
  69. package/dist/components/Stat.svelte +253 -0
  70. package/dist/components/Stat.svelte.d.ts +46 -0
  71. package/dist/components/Steps.svelte +189 -0
  72. package/dist/components/Steps.svelte.d.ts +31 -0
  73. package/dist/components/Switch.svelte +222 -0
  74. package/dist/components/Switch.svelte.d.ts +18 -0
  75. package/dist/components/Table.svelte +504 -0
  76. package/dist/components/Table.svelte.d.ts +92 -0
  77. package/dist/components/Tabs.svelte +246 -0
  78. package/dist/components/Tabs.svelte.d.ts +44 -0
  79. package/dist/components/Tag.svelte +40 -0
  80. package/dist/components/Tag.svelte.d.ts +15 -0
  81. package/dist/components/TextArea.svelte +272 -0
  82. package/dist/components/TextArea.svelte.d.ts +28 -0
  83. package/dist/components/TextField.svelte +256 -0
  84. package/dist/components/TextField.svelte.d.ts +32 -0
  85. package/dist/components/Timeline.svelte +156 -0
  86. package/dist/components/Timeline.svelte.d.ts +30 -0
  87. package/dist/components/Toaster.svelte +411 -0
  88. package/dist/components/Toaster.svelte.d.ts +11 -0
  89. package/dist/components/Tooltip.svelte +200 -0
  90. package/dist/components/Tooltip.svelte.d.ts +38 -0
  91. package/dist/components/TooltipGroup.svelte +46 -0
  92. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  93. package/dist/components/internal/Scrim.svelte +52 -0
  94. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  95. package/dist/components/internal/Sheet.svelte +263 -0
  96. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  97. package/dist/components/internal/fade-in.d.ts +10 -0
  98. package/dist/components/internal/fade-in.js +20 -0
  99. package/dist/components/internal/groups.d.ts +14 -0
  100. package/dist/components/internal/groups.js +23 -0
  101. package/dist/components/internal/resolve.d.ts +8 -0
  102. package/dist/components/internal/resolve.js +247 -0
  103. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  104. package/dist/components/internal/swipe-dismiss.js +220 -0
  105. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  106. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  107. package/dist/components/toast.svelte.d.ts +57 -0
  108. package/dist/components/toast.svelte.js +84 -0
  109. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  110. package/dist/fonts/OFL.txt +93 -0
  111. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  112. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  113. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  114. package/dist/fonts.css +71 -0
  115. package/dist/index.d.ts +45 -0
  116. package/dist/index.js +45 -0
  117. package/dist/motion/Motion.svelte +15 -0
  118. package/dist/motion/Motion.svelte.d.ts +9 -0
  119. package/dist/motion/context.d.ts +18 -0
  120. package/dist/motion/context.js +26 -0
  121. package/dist/motion/index.d.ts +3 -0
  122. package/dist/motion/index.js +3 -0
  123. package/dist/motion/transitions.d.ts +31 -0
  124. package/dist/motion/transitions.js +112 -0
  125. package/dist/tokens.css +283 -0
  126. package/package.json +76 -6
@@ -0,0 +1,256 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLInputAttributes } from 'svelte/elements';
3
+
4
+ /** The text input types the well suits. For numbers use `text` with `inputmode="numeric"`. */
5
+ export type TextFieldType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
6
+
7
+ /** From which width the label sits beside the well: 600px, 900px or 1200px. */
8
+ export type TextFieldBreakpoint = 'medium' | 'expanded' | 'large';
9
+
10
+ /** Attributes and handlers not listed here go to the `<input>`. */
11
+ export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
12
+ /** The caps field label above the well. Required: a placeholder is never the only label. */
13
+ label: string;
14
+ /** The text. Bindable. */
15
+ value?: string;
16
+ type?: TextFieldType;
17
+ /** Guidance under the well in `--muted`, linked by `aria-describedby`. */
18
+ hint?: string;
19
+ /**
20
+ * The error to show under the well, with an icon, linked by
21
+ * `aria-describedby`; it sets `aria-invalid`. Say how to fix the
22
+ * problem. The app validates; the field shows what it is given.
23
+ */
24
+ error?: string;
25
+ /**
26
+ * The label sits above the well by default. `true` puts it beside the
27
+ * well at every width; a breakpoint puts it beside from that width up.
28
+ * The hint and the error stay under the well.
29
+ */
30
+ labelBeside?: boolean | TextFieldBreakpoint;
31
+ /** The `<input>`, for example to focus the first error on submit. */
32
+ ref?: HTMLInputElement | null;
33
+ };
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ import { untrack } from 'svelte';
38
+ import { Label } from 'bits-ui';
39
+ import { fadeIn } from './internal/fade-in.js';
40
+
41
+ const uid = $props.id();
42
+
43
+ let {
44
+ label,
45
+ value = $bindable(''),
46
+ type = 'text',
47
+ hint,
48
+ error,
49
+ labelBeside = false,
50
+ id = uid,
51
+ disabled,
52
+ ref = $bindable(null),
53
+ class: className,
54
+ 'aria-describedby': describedBy,
55
+ 'aria-invalid': ariaInvalid,
56
+ ...rest
57
+ }: TextFieldProps = $props();
58
+
59
+ const hintId = $derived(`${id}-hint`);
60
+ const errorId = $derived(`${id}-error`);
61
+ // The error first, so a screen reader reads it before the hint.
62
+ const describedByIds = $derived(
63
+ [error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
64
+ undefined
65
+ );
66
+
67
+ // ---- Short motion: the error (BRIEF, --dur-message) --------------------
68
+
69
+ // An error that appears after the first render fades in from 0 over
70
+ // --dur-message, and the well's edge fades to --danger with it (the CSS
71
+ // below). An error that clears goes at once, and so does the red edge. A
72
+ // field that renders or mounts with an error, such as one in a dialog
73
+ // that opens, shows it at once. 0ms is a snap.
74
+ let root: HTMLDivElement | null = $state(null);
75
+ let errorShown = untrack(() => !!error);
76
+ $effect(() => {
77
+ const shown = !!error;
78
+ untrack(() => {
79
+ const appeared = shown && !errorShown;
80
+ errorShown = shown;
81
+ const message = appeared ? root?.querySelector(':scope > .error') : null;
82
+ if (message) fadeIn(message, '--dur-message');
83
+ });
84
+ });
85
+ </script>
86
+
87
+ <!--
88
+ A single-line text input in the black well, with a plain caps label above
89
+ it (Bits UI Label), an optional hint and an inline error below. Extra
90
+ attributes (`name`, `inputmode`, `autocomplete`, `enterkeyhint`,
91
+ `required`), handlers and `class` go to the <input>.
92
+ -->
93
+ <div
94
+ bind:this={root}
95
+ class="hp-text-field"
96
+ data-invalid={error ? '' : undefined}
97
+ data-disabled={disabled ? '' : undefined}
98
+ data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
99
+ >
100
+ <Label.Root for={id}>
101
+ {#snippet child({ props })}
102
+ <label {...props} class="hp-label">{label}</label>
103
+ {/snippet}
104
+ </Label.Root>
105
+ <input
106
+ {...rest}
107
+ bind:this={ref}
108
+ bind:value
109
+ {id}
110
+ {type}
111
+ {disabled}
112
+ class={['hp-well', className]}
113
+ aria-describedby={describedByIds}
114
+ aria-invalid={error ? true : ariaInvalid}
115
+ />
116
+ {#if hint}
117
+ <p class="hint hp-caption" id={hintId}>{hint}</p>
118
+ {/if}
119
+ {#if error}
120
+ <p class="error hp-caption" id={errorId}>
121
+ <span class="icon" aria-hidden="true">
122
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
123
+ <path d="M1.5 1.5h13v13h-13z" />
124
+ <path d="M8 4v4M8 10v2" stroke-width="2" />
125
+ </svg>
126
+ </span>
127
+ <span>{error}</span>
128
+ </p>
129
+ {/if}
130
+ </div>
131
+
132
+ <style>
133
+ .hp-text-field {
134
+ display: grid;
135
+ gap: var(--space-1);
136
+ min-width: 0;
137
+ }
138
+ label {
139
+ color: var(--muted);
140
+ }
141
+ input {
142
+ display: block;
143
+ width: 100%;
144
+ min-width: 0;
145
+ }
146
+ input::placeholder {
147
+ color: var(--muted);
148
+ opacity: 1;
149
+ }
150
+
151
+ /* Invalid: the boundary turns --danger (5.9:1 on black), never alone:
152
+ the icon and the message say what is wrong. Keyed on the root's
153
+ data-invalid, set by `error`, so an aria-invalid the app passes
154
+ without a message draws no red border. The edge fades to --danger
155
+ over --dur-message with the message; the transition belongs to this
156
+ rule, so the edge goes back at once when the error clears. The hover
157
+ fill keeps base.css's timing. */
158
+ [data-invalid] input {
159
+ border-color: var(--danger);
160
+ transition:
161
+ background-color var(--dur-hover) var(--settle),
162
+ border-color var(--dur-message) var(--settle);
163
+ }
164
+ [data-invalid] input:disabled {
165
+ border-color: var(--border);
166
+ }
167
+
168
+ .hint {
169
+ margin: 0;
170
+ color: var(--muted);
171
+ }
172
+ .error {
173
+ display: flex;
174
+ gap: var(--space-2);
175
+ margin: 0;
176
+ color: var(--danger);
177
+ }
178
+ /* The first message starts 8px under the well, clear of the focus
179
+ contour (2px, drawn 4px outside the well). */
180
+ input + .hint,
181
+ input + .error {
182
+ margin-top: var(--space-1);
183
+ }
184
+ /* One line box high, so the icon centres on the first line of the
185
+ message however it wraps. */
186
+ .icon {
187
+ display: flex;
188
+ flex: none;
189
+ align-items: center;
190
+ height: 1lh;
191
+ }
192
+ .icon svg {
193
+ display: block;
194
+ }
195
+
196
+ /* Beside the well, the label keeps its one line in the first column; the
197
+ well, hint and error share the second. */
198
+ [data-label-beside='always'] {
199
+ grid-template-columns: auto minmax(0, 1fr);
200
+ column-gap: var(--space-2);
201
+ }
202
+ [data-label-beside='always'] label {
203
+ grid-row: 1;
204
+ align-self: center;
205
+ white-space: nowrap;
206
+ }
207
+ [data-label-beside='always'] input,
208
+ [data-label-beside='always'] p {
209
+ grid-column: 2;
210
+ }
211
+ @media (min-width: 600px) {
212
+ [data-label-beside='medium'] {
213
+ grid-template-columns: auto minmax(0, 1fr);
214
+ column-gap: var(--space-2);
215
+ }
216
+ [data-label-beside='medium'] label {
217
+ grid-row: 1;
218
+ align-self: center;
219
+ white-space: nowrap;
220
+ }
221
+ [data-label-beside='medium'] input,
222
+ [data-label-beside='medium'] p {
223
+ grid-column: 2;
224
+ }
225
+ }
226
+ @media (min-width: 900px) {
227
+ [data-label-beside='expanded'] {
228
+ grid-template-columns: auto minmax(0, 1fr);
229
+ column-gap: var(--space-2);
230
+ }
231
+ [data-label-beside='expanded'] label {
232
+ grid-row: 1;
233
+ align-self: center;
234
+ white-space: nowrap;
235
+ }
236
+ [data-label-beside='expanded'] input,
237
+ [data-label-beside='expanded'] p {
238
+ grid-column: 2;
239
+ }
240
+ }
241
+ @media (min-width: 1200px) {
242
+ [data-label-beside='large'] {
243
+ grid-template-columns: auto minmax(0, 1fr);
244
+ column-gap: var(--space-2);
245
+ }
246
+ [data-label-beside='large'] label {
247
+ grid-row: 1;
248
+ align-self: center;
249
+ white-space: nowrap;
250
+ }
251
+ [data-label-beside='large'] input,
252
+ [data-label-beside='large'] p {
253
+ grid-column: 2;
254
+ }
255
+ }
256
+ </style>
@@ -0,0 +1,32 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ /** The text input types the well suits. For numbers use `text` with `inputmode="numeric"`. */
3
+ export type TextFieldType = 'text' | 'email' | 'password' | 'search' | 'tel' | 'url';
4
+ /** From which width the label sits beside the well: 600px, 900px or 1200px. */
5
+ export type TextFieldBreakpoint = 'medium' | 'expanded' | 'large';
6
+ /** Attributes and handlers not listed here go to the `<input>`. */
7
+ export type TextFieldProps = Omit<HTMLInputAttributes, 'type' | 'value' | 'children'> & {
8
+ /** The caps field label above the well. Required: a placeholder is never the only label. */
9
+ label: string;
10
+ /** The text. Bindable. */
11
+ value?: string;
12
+ type?: TextFieldType;
13
+ /** Guidance under the well in `--muted`, linked by `aria-describedby`. */
14
+ hint?: string;
15
+ /**
16
+ * The error to show under the well, with an icon, linked by
17
+ * `aria-describedby`; it sets `aria-invalid`. Say how to fix the
18
+ * problem. The app validates; the field shows what it is given.
19
+ */
20
+ error?: string;
21
+ /**
22
+ * The label sits above the well by default. `true` puts it beside the
23
+ * well at every width; a breakpoint puts it beside from that width up.
24
+ * The hint and the error stay under the well.
25
+ */
26
+ labelBeside?: boolean | TextFieldBreakpoint;
27
+ /** The `<input>`, for example to focus the first error on submit. */
28
+ ref?: HTMLInputElement | null;
29
+ };
30
+ declare const TextField: import("svelte").Component<TextFieldProps, {}, "value" | "ref">;
31
+ type TextField = ReturnType<typeof TextField>;
32
+ export default TextField;
@@ -0,0 +1,156 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLOlAttributes } from 'svelte/elements';
3
+
4
+ /** A status hue for an event's node: `ok` done, `warn` waiting or needs you, `danger` failed. */
5
+ export type TimelineTone = 'ok' | 'warn' | 'danger';
6
+
7
+ export type TimelineEvent = {
8
+ /** What happened, as the app or a user wrote it. */
9
+ title: string;
10
+ /** A supporting line in `--muted`. */
11
+ detail?: string;
12
+ /** The date as shown. */
13
+ date: string;
14
+ /** The time as shown, after the date. */
15
+ time?: string;
16
+ /**
17
+ * The machine-readable moment for `<time datetime>`. Defaults to
18
+ * `date`, or `dateTtime` with a time, so ISO dates need nothing else.
19
+ */
20
+ datetime?: string;
21
+ /**
22
+ * Colours the node in a status hue. The title must say the same, because
23
+ * colour is never the only signal.
24
+ */
25
+ tone?: TimelineTone;
26
+ };
27
+
28
+ export type TimelineProps = Omit<HTMLOlAttributes, 'children'> & {
29
+ /** Oldest first or newest first, in the order given. */
30
+ events: TimelineEvent[];
31
+ };
32
+ </script>
33
+
34
+ <script lang="ts">
35
+ import { onMount, untrack } from 'svelte';
36
+ import { useMotion } from '../motion/index.js';
37
+
38
+ let { events, class: className, ...rest }: TimelineProps = $props();
39
+
40
+ // Expressive stagger (motion rule 7), as in Table: only the events of the
41
+ // first render enter, by their index. Each <li> carries its node and its
42
+ // rail segment, so they enter with it. Any change to `events` (a new array
43
+ // or a changed length) ends the entrance, and so does its last animation.
44
+ const motion = useMotion();
45
+ const firstEvents = untrack(() => events);
46
+ const firstCount = untrack(() => events.length);
47
+ let entering = $state.raw(motion.expressive);
48
+ let root: HTMLOListElement;
49
+
50
+ $effect.pre(() => {
51
+ if (events !== firstEvents || events.length !== firstCount) entering = false;
52
+ });
53
+
54
+ onMount(() => {
55
+ if (!entering) return;
56
+ const running = root
57
+ .getAnimations({ subtree: true })
58
+ .filter((a) => a instanceof CSSAnimation && a.animationName === 'hp-enter');
59
+ Promise.allSettled(running.map((a) => a.finished)).then(() => {
60
+ entering = false;
61
+ });
62
+ });
63
+ </script>
64
+
65
+ <!--
66
+ Square nodes on a 2px titanium rail (BRIEF, visual rule 7; da3-60), in
67
+ Geist Mono. Extra attributes and `class` go to the <ol>.
68
+ -->
69
+ <ol {...rest} bind:this={root} class={['hp-timeline', className]}>
70
+ {#each events as event, i}
71
+ <li data-tone={event.tone} class={[entering && 'hp-enter']} style:--i={entering ? i : undefined}>
72
+ <span>{event.title}</span>
73
+ {#if event.detail}
74
+ <span class="detail">{event.detail}</span>
75
+ {/if}
76
+ <span class="when"
77
+ ><time datetime={event.datetime ?? (event.time ? `${event.date}T${event.time}` : event.date)}
78
+ >{event.date}</time
79
+ >{#if event.time}<span>{event.time}</span>{/if}</span
80
+ >
81
+ </li>
82
+ {/each}
83
+ </ol>
84
+
85
+ <style>
86
+ /* 16px on touch, the dense 14px on fine pointers. A long identifier or
87
+ path breaks anywhere rather than widen the page. */
88
+ .hp-timeline {
89
+ margin: 0;
90
+ padding: 0 0 0 var(--space-8);
91
+ font-family: var(--mono);
92
+ font-size: var(--type-body);
93
+ line-height: 24px;
94
+ list-style: none;
95
+ overflow-wrap: anywhere;
96
+ }
97
+ @media (pointer: fine) {
98
+ .hp-timeline {
99
+ font-size: var(--type-dense);
100
+ line-height: 20px;
101
+ }
102
+ }
103
+
104
+ li {
105
+ position: relative;
106
+ display: grid;
107
+ padding-bottom: var(--space-6);
108
+ }
109
+ li:last-child {
110
+ padding-bottom: 0;
111
+ }
112
+
113
+ /* The node: a 12px square, titanium or the event's status hue, centred on
114
+ the first line (4px from the top at 20px lines, 6px at 24px). */
115
+ li::before {
116
+ content: '';
117
+ position: absolute;
118
+ top: calc((1lh - 12px) / 2);
119
+ left: calc(-1 * var(--space-8));
120
+ width: 12px;
121
+ height: 12px;
122
+ background: var(--accent);
123
+ }
124
+ li[data-tone='ok']::before {
125
+ background: var(--ok);
126
+ }
127
+ li[data-tone='warn']::before {
128
+ background: var(--warn);
129
+ }
130
+ li[data-tone='danger']::before {
131
+ background: var(--danger);
132
+ }
133
+
134
+ /* The rail: 2px, centred under the node, from its bottom edge down to the
135
+ next node's top edge. */
136
+ li:not(:last-child)::after {
137
+ content: '';
138
+ position: absolute;
139
+ top: calc((1lh + 12px) / 2);
140
+ bottom: calc((12px - 1lh) / 2);
141
+ /* matches the approved specimen */
142
+ left: calc(-1 * var(--space-8) + 5px);
143
+ /* matches the approved specimen */
144
+ width: 2px;
145
+ background: var(--accent);
146
+ }
147
+
148
+ .detail {
149
+ color: var(--muted);
150
+ }
151
+ .when {
152
+ display: flex;
153
+ /* matches the approved specimen */
154
+ gap: 4ch;
155
+ }
156
+ </style>
@@ -0,0 +1,30 @@
1
+ import type { HTMLOlAttributes } from 'svelte/elements';
2
+ /** A status hue for an event's node: `ok` done, `warn` waiting or needs you, `danger` failed. */
3
+ export type TimelineTone = 'ok' | 'warn' | 'danger';
4
+ export type TimelineEvent = {
5
+ /** What happened, as the app or a user wrote it. */
6
+ title: string;
7
+ /** A supporting line in `--muted`. */
8
+ detail?: string;
9
+ /** The date as shown. */
10
+ date: string;
11
+ /** The time as shown, after the date. */
12
+ time?: string;
13
+ /**
14
+ * The machine-readable moment for `<time datetime>`. Defaults to
15
+ * `date`, or `dateTtime` with a time, so ISO dates need nothing else.
16
+ */
17
+ datetime?: string;
18
+ /**
19
+ * Colours the node in a status hue. The title must say the same, because
20
+ * colour is never the only signal.
21
+ */
22
+ tone?: TimelineTone;
23
+ };
24
+ export type TimelineProps = Omit<HTMLOlAttributes, 'children'> & {
25
+ /** Oldest first or newest first, in the order given. */
26
+ events: TimelineEvent[];
27
+ };
28
+ declare const Timeline: import("svelte").Component<TimelineProps, {}, "">;
29
+ type Timeline = ReturnType<typeof Timeline>;
30
+ export default Timeline;