@mystravil/hardpoint 0.2.0 → 0.3.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 (54) hide show
  1. package/CHANGELOG.md +51 -38
  2. package/LICENSE +25 -25
  3. package/README.md +67 -64
  4. package/dist/base.css +625 -625
  5. package/dist/components/AppShell.svelte +698 -698
  6. package/dist/components/Button.svelte +221 -174
  7. package/dist/components/Button.svelte.d.ts +18 -2
  8. package/dist/components/Checkbox.svelte +235 -235
  9. package/dist/components/CheckboxGroup.svelte +207 -207
  10. package/dist/components/Combobox.svelte +901 -901
  11. package/dist/components/Dialog.svelte +359 -359
  12. package/dist/components/Disclosure.svelte +201 -201
  13. package/dist/components/EmptyState.svelte +88 -88
  14. package/dist/components/ErrorState.svelte +135 -135
  15. package/dist/components/ImageViewer.svelte +764 -764
  16. package/dist/components/KeyValueList.svelte +114 -114
  17. package/dist/components/ListDetail.svelte +218 -218
  18. package/dist/components/LoadRegion.svelte +219 -219
  19. package/dist/components/MediaFrame.svelte +110 -110
  20. package/dist/components/MediaImage.svelte +256 -256
  21. package/dist/components/Menu.svelte +275 -258
  22. package/dist/components/MenuCheckboxItem.svelte +119 -119
  23. package/dist/components/MenuGroup.svelte +50 -50
  24. package/dist/components/MenuItem.svelte +94 -94
  25. package/dist/components/MenuRadioGroup.svelte +62 -62
  26. package/dist/components/MenuRadioItem.svelte +110 -110
  27. package/dist/components/MenuSeparator.svelte +28 -28
  28. package/dist/components/Meter.svelte +283 -283
  29. package/dist/components/NativeSelect.svelte +135 -135
  30. package/dist/components/Notice.svelte +262 -262
  31. package/dist/components/Popover.svelte +222 -222
  32. package/dist/components/Progress.svelte +157 -157
  33. package/dist/components/RadioGroup.svelte +440 -440
  34. package/dist/components/Select.svelte +471 -471
  35. package/dist/components/Skeleton.svelte +149 -149
  36. package/dist/components/Spinner.svelte +118 -118
  37. package/dist/components/Stat.svelte +253 -253
  38. package/dist/components/Steps.svelte +189 -189
  39. package/dist/components/Switch.svelte +222 -222
  40. package/dist/components/Table.svelte +504 -504
  41. package/dist/components/Tabs.svelte +246 -246
  42. package/dist/components/Tag.svelte +40 -40
  43. package/dist/components/TextArea.svelte +272 -272
  44. package/dist/components/TextField.svelte +396 -256
  45. package/dist/components/TextField.svelte.d.ts +16 -1
  46. package/dist/components/Timeline.svelte +156 -156
  47. package/dist/components/Toaster.svelte +411 -411
  48. package/dist/components/Tooltip.svelte +200 -200
  49. package/dist/components/TooltipGroup.svelte +46 -46
  50. package/dist/components/internal/Scrim.svelte +52 -52
  51. package/dist/components/internal/Sheet.svelte +263 -263
  52. package/dist/fonts.css +71 -71
  53. package/dist/tokens.css +283 -283
  54. package/package.json +81 -76
@@ -1,246 +1,246 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import { Tabs as BitsTabs } from 'bits-ui';
4
-
5
- export type TabsItem = {
6
- /** Identifies the tab; the bound `value` holds the selected one. */
7
- value: string;
8
- /** The caps label. A short fixed string the app defines, never user data. */
9
- label: string;
10
- /** Shown in `--muted`; it cannot be selected and arrow keys skip it. */
11
- disabled?: boolean;
12
- };
13
-
14
- /** Tabs may carry extra fields (a count, an icon name) that the `panel` snippet reads. */
15
- export type TabsProps<T extends TabsItem = TabsItem> = Omit<
16
- BitsTabs.RootProps,
17
- 'value' | 'orientation' | 'child' | 'children'
18
- > & {
19
- tabs: T[];
20
- /** The selected tab's `value`. Bindable; defaults to the first tab that is not disabled. */
21
- value?: string;
22
- /** The tab list's accessible name, for example "Project sections". */
23
- label: string;
24
- /** Renders one tab's panel. Every panel stays mounted; the hidden ones carry `hidden`. */
25
- panel: Snippet<[T]>;
26
- };
27
- </script>
28
-
29
- <script lang="ts" generics="T extends TabsItem = TabsItem">
30
- import { untrack } from 'svelte';
31
- import { fadeIn } from './internal/fade-in.js';
32
-
33
- let {
34
- tabs,
35
- value = $bindable(tabs.find((tab) => !tab.disabled)?.value ?? ''),
36
- label,
37
- panel,
38
- class: className,
39
- ref = $bindable(null),
40
- onValueChange,
41
- ...rest
42
- }: TabsProps<T> = $props();
43
-
44
- let list: HTMLElement | null = $state(null);
45
-
46
- // Shows the selected tab whole inside the list, with part of the next one
47
- // (the list's scroll-padding). It scrolls the list alone, never the page
48
- // (scrollIntoView would), and jumps: selection snaps (motion rule 3).
49
- function reveal(list: HTMLElement, selected: string) {
50
- const tab = Array.from(list.children).find(
51
- (el): el is HTMLElement => el instanceof HTMLElement && el.dataset.value === selected
52
- );
53
- if (!tab) return;
54
- const style = getComputedStyle(list);
55
- // The tab's edges in the list's scroll coordinates, from rects: offsetLeft
56
- // and offsetWidth round to whole pixels. The target rounds outward, so
57
- // the padding never comes up a fraction short.
58
- const listLeft = list.getBoundingClientRect().left + list.clientLeft;
59
- const rect = tab.getBoundingClientRect();
60
- const left = rect.left - listLeft + list.scrollLeft;
61
- const start = Math.floor(left - (parseFloat(style.scrollPaddingLeft) || 0));
62
- const end = Math.ceil(left + rect.width + (parseFloat(style.scrollPaddingRight) || 0));
63
- if (start < list.scrollLeft) {
64
- list.scrollTo({ left: start, behavior: 'instant' });
65
- } else if (end > list.scrollLeft + list.clientWidth) {
66
- list.scrollTo({ left: end - list.clientWidth, behavior: 'instant' });
67
- }
68
- }
69
-
70
- // On the first render and whenever the selection changes.
71
- $effect(() => {
72
- if (list) reveal(list, value);
73
- });
74
-
75
- // Again whenever the list or a tab changes size: the web font arrives, the
76
- // screen turns, or a hidden parent (another tab panel) shows the list. The
77
- // watcher restarts whenever the set of tabs changes, not only their number,
78
- // so a tab an app swaps in for another is watched too (each tab element is
79
- // keyed by its value).
80
- $effect(() => {
81
- const el = list;
82
- const values = tabs.map((tab) => tab.value);
83
- if (!el || values.length === 0) return;
84
- const observer = new ResizeObserver(() => reveal(el, value));
85
- observer.observe(el);
86
- for (const tab of el.children) observer.observe(tab);
87
- return () => observer.disconnect();
88
- });
89
-
90
- // ---- Short motion: the panel (BRIEF, --dur-panel) ----------------------
91
-
92
- // A panel chosen by a pointer or a touch fades in from 0 over
93
- // --dur-panel. A panel chosen by a key (arrows, Home, End, Enter, Space)
94
- // or by the app through `value` cuts, so holding an arrow key through the
95
- // tabs never starts a fade (the frequency check). The input: the tab list
96
- // keeps the last one it saw, from pointerdown and keydown in the capture
97
- // phase, before Bits UI acts on them. Bits UI calls onValueChange only for
98
- // a selection the user makes, never for a `value` the app sets, so the
99
- // change notes there whether a pointer made it, and the effect below
100
- // fades the new panel once it shows. A new selection cancels a fade that
101
- // still runs. 0ms is a cut, as before the token.
102
- let input: 'pointer' | 'key' | null = null;
103
- let fadeTo: string | null = null;
104
- let fade: Animation | null = null;
105
-
106
- function changed(next: string) {
107
- fadeTo = input === 'pointer' ? next : null;
108
- onValueChange?.(next);
109
- }
110
-
111
- $effect(() => {
112
- const selected = value;
113
- untrack(() => {
114
- fade?.cancel();
115
- fade = null;
116
- const chosen = fadeTo === selected;
117
- fadeTo = null;
118
- if (!chosen || !ref) return;
119
- const shown = Array.from(ref.children).find(
120
- (el): el is HTMLElement =>
121
- el instanceof HTMLElement && el.classList.contains('hp-tabs-panel') && el.dataset.value === selected
122
- );
123
- if (shown) fade = fadeIn(shown, '--dur-panel');
124
- });
125
- });
126
- </script>
127
-
128
- <!--
129
- Bits UI Tabs, rendered through `child` snippets so every element is this
130
- component's markup and its styles stay scoped. Extra attributes, `class`
131
- and Bits UI root props (`activationMode`, `loop`, `disabled`,
132
- `onValueChange`) go to the root `div.hp-tabs`. Parts: `.hp-tabs-list`
133
- (role tablist), `.hp-tabs-tab` (role tab, with Bits UI's `data-state` and
134
- `data-value`), `.hp-tabs-panel` (role tabpanel).
135
- -->
136
- <BitsTabs.Root bind:value bind:ref onValueChange={changed} {...rest}>
137
- {#snippet child({ props })}
138
- <div {...props} class={['hp-tabs', className]}>
139
- <BitsTabs.List bind:ref={list} aria-label={label}>
140
- {#snippet child({ props })}
141
- <div
142
- {...props}
143
- class="hp-tabs-list"
144
- onpointerdowncapture={() => (input = 'pointer')}
145
- onkeydowncapture={() => (input = 'key')}
146
- >
147
- {#each tabs as tab (tab.value)}
148
- <BitsTabs.Trigger value={tab.value} disabled={tab.disabled}>
149
- {#snippet child({ props })}
150
- <button {...props} class="hp-tabs-tab hp-label hp-focus-inset">{tab.label}</button>
151
- {/snippet}
152
- </BitsTabs.Trigger>
153
- {/each}
154
- </div>
155
- {/snippet}
156
- </BitsTabs.List>
157
- {#each tabs as tab (tab.value)}
158
- <BitsTabs.Content value={tab.value}>
159
- {#snippet child({ props })}
160
- <div {...props} class="hp-tabs-panel">{@render panel(tab)}</div>
161
- {/snippet}
162
- </BitsTabs.Content>
163
- {/each}
164
- </div>
165
- {/snippet}
166
- </BitsTabs.Root>
167
-
168
- <style>
169
- /* In a flex or grid parent the tabs never grow wider than their column:
170
- the list scrolls instead of widening the page. */
171
- .hp-tabs {
172
- min-width: 0;
173
- }
174
-
175
- /* One row that scrolls sideways when the tabs do not fit. It reaches
176
- --bleed past the text edge, as the table band does, and each tab pads
177
- by --bleed, so the first label keeps the page's text edge. */
178
- .hp-tabs-list {
179
- display: flex;
180
- margin-inline: calc(-1 * var(--bleed));
181
- overflow-x: auto;
182
- overscroll-behavior-x: contain;
183
- /* Scrolling a tab into view leaves part of the next one showing. */
184
- scroll-padding-inline: var(--space-8);
185
- /* A thin bar on fine pointers, where a wheel cannot scroll sideways
186
- without one; touch swipes. */
187
- scrollbar-width: thin;
188
- }
189
- @media (pointer: coarse) {
190
- .hp-tabs-list {
191
- scrollbar-width: none;
192
- }
193
- }
194
-
195
- /* The visible box is the hit area: --control-height high (44px on
196
- coarse pointers) and never narrower than --hit-target. */
197
- .hp-tabs-tab {
198
- display: inline-flex;
199
- flex: none;
200
- align-items: center;
201
- justify-content: center;
202
- box-sizing: border-box;
203
- min-width: var(--hit-target);
204
- min-height: var(--control-height);
205
- padding: 0 var(--bleed);
206
- border: 0;
207
- background: transparent;
208
- color: var(--text);
209
- white-space: nowrap;
210
- cursor: pointer;
211
- -webkit-tap-highlight-color: transparent;
212
- }
213
-
214
- /* Selected: a 3px titanium edge on the side facing the panel and the
215
- selected tint, text unchanged. Selection snaps, so nothing on a tab
216
- transitions: a fading hover tint would also animate the tab an arrow
217
- key just left while the pointer rests on it. */
218
- .hp-tabs-tab[data-state='active'] {
219
- background: var(--surface-selected);
220
- box-shadow: inset 0 -3px 0 var(--accent);
221
- }
222
-
223
- /* Hover: fine pointers only, never on the selected tab. */
224
- @media (hover: hover) and (pointer: fine) {
225
- .hp-tabs-tab[data-state='inactive']:hover:not(:disabled) {
226
- background: var(--surface-hover);
227
- }
228
- }
229
-
230
- /* Press: one step past hover, so it reads on a phone too. */
231
- .hp-tabs-tab[data-state='inactive']:active:not(:disabled) {
232
- background: var(--surface-raised);
233
- }
234
-
235
- .hp-tabs-tab:disabled {
236
- color: var(--muted);
237
- cursor: not-allowed;
238
- }
239
-
240
- /* A panel a pointer chose fades in over --dur-panel (see the script);
241
- otherwise it cuts. Margin, not padding, keeps the panel's focus
242
- contour clear of the tabs. */
243
- .hp-tabs-panel {
244
- margin-top: var(--space-6);
245
- }
246
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import { Tabs as BitsTabs } from 'bits-ui';
4
+
5
+ export type TabsItem = {
6
+ /** Identifies the tab; the bound `value` holds the selected one. */
7
+ value: string;
8
+ /** The caps label. A short fixed string the app defines, never user data. */
9
+ label: string;
10
+ /** Shown in `--muted`; it cannot be selected and arrow keys skip it. */
11
+ disabled?: boolean;
12
+ };
13
+
14
+ /** Tabs may carry extra fields (a count, an icon name) that the `panel` snippet reads. */
15
+ export type TabsProps<T extends TabsItem = TabsItem> = Omit<
16
+ BitsTabs.RootProps,
17
+ 'value' | 'orientation' | 'child' | 'children'
18
+ > & {
19
+ tabs: T[];
20
+ /** The selected tab's `value`. Bindable; defaults to the first tab that is not disabled. */
21
+ value?: string;
22
+ /** The tab list's accessible name, for example "Project sections". */
23
+ label: string;
24
+ /** Renders one tab's panel. Every panel stays mounted; the hidden ones carry `hidden`. */
25
+ panel: Snippet<[T]>;
26
+ };
27
+ </script>
28
+
29
+ <script lang="ts" generics="T extends TabsItem = TabsItem">
30
+ import { untrack } from 'svelte';
31
+ import { fadeIn } from './internal/fade-in.js';
32
+
33
+ let {
34
+ tabs,
35
+ value = $bindable(tabs.find((tab) => !tab.disabled)?.value ?? ''),
36
+ label,
37
+ panel,
38
+ class: className,
39
+ ref = $bindable(null),
40
+ onValueChange,
41
+ ...rest
42
+ }: TabsProps<T> = $props();
43
+
44
+ let list: HTMLElement | null = $state(null);
45
+
46
+ // Shows the selected tab whole inside the list, with part of the next one
47
+ // (the list's scroll-padding). It scrolls the list alone, never the page
48
+ // (scrollIntoView would), and jumps: selection snaps (motion rule 3).
49
+ function reveal(list: HTMLElement, selected: string) {
50
+ const tab = Array.from(list.children).find(
51
+ (el): el is HTMLElement => el instanceof HTMLElement && el.dataset.value === selected
52
+ );
53
+ if (!tab) return;
54
+ const style = getComputedStyle(list);
55
+ // The tab's edges in the list's scroll coordinates, from rects: offsetLeft
56
+ // and offsetWidth round to whole pixels. The target rounds outward, so
57
+ // the padding never comes up a fraction short.
58
+ const listLeft = list.getBoundingClientRect().left + list.clientLeft;
59
+ const rect = tab.getBoundingClientRect();
60
+ const left = rect.left - listLeft + list.scrollLeft;
61
+ const start = Math.floor(left - (parseFloat(style.scrollPaddingLeft) || 0));
62
+ const end = Math.ceil(left + rect.width + (parseFloat(style.scrollPaddingRight) || 0));
63
+ if (start < list.scrollLeft) {
64
+ list.scrollTo({ left: start, behavior: 'instant' });
65
+ } else if (end > list.scrollLeft + list.clientWidth) {
66
+ list.scrollTo({ left: end - list.clientWidth, behavior: 'instant' });
67
+ }
68
+ }
69
+
70
+ // On the first render and whenever the selection changes.
71
+ $effect(() => {
72
+ if (list) reveal(list, value);
73
+ });
74
+
75
+ // Again whenever the list or a tab changes size: the web font arrives, the
76
+ // screen turns, or a hidden parent (another tab panel) shows the list. The
77
+ // watcher restarts whenever the set of tabs changes, not only their number,
78
+ // so a tab an app swaps in for another is watched too (each tab element is
79
+ // keyed by its value).
80
+ $effect(() => {
81
+ const el = list;
82
+ const values = tabs.map((tab) => tab.value);
83
+ if (!el || values.length === 0) return;
84
+ const observer = new ResizeObserver(() => reveal(el, value));
85
+ observer.observe(el);
86
+ for (const tab of el.children) observer.observe(tab);
87
+ return () => observer.disconnect();
88
+ });
89
+
90
+ // ---- Short motion: the panel (BRIEF, --dur-panel) ----------------------
91
+
92
+ // A panel chosen by a pointer or a touch fades in from 0 over
93
+ // --dur-panel. A panel chosen by a key (arrows, Home, End, Enter, Space)
94
+ // or by the app through `value` cuts, so holding an arrow key through the
95
+ // tabs never starts a fade (the frequency check). The input: the tab list
96
+ // keeps the last one it saw, from pointerdown and keydown in the capture
97
+ // phase, before Bits UI acts on them. Bits UI calls onValueChange only for
98
+ // a selection the user makes, never for a `value` the app sets, so the
99
+ // change notes there whether a pointer made it, and the effect below
100
+ // fades the new panel once it shows. A new selection cancels a fade that
101
+ // still runs. 0ms is a cut, as before the token.
102
+ let input: 'pointer' | 'key' | null = null;
103
+ let fadeTo: string | null = null;
104
+ let fade: Animation | null = null;
105
+
106
+ function changed(next: string) {
107
+ fadeTo = input === 'pointer' ? next : null;
108
+ onValueChange?.(next);
109
+ }
110
+
111
+ $effect(() => {
112
+ const selected = value;
113
+ untrack(() => {
114
+ fade?.cancel();
115
+ fade = null;
116
+ const chosen = fadeTo === selected;
117
+ fadeTo = null;
118
+ if (!chosen || !ref) return;
119
+ const shown = Array.from(ref.children).find(
120
+ (el): el is HTMLElement =>
121
+ el instanceof HTMLElement && el.classList.contains('hp-tabs-panel') && el.dataset.value === selected
122
+ );
123
+ if (shown) fade = fadeIn(shown, '--dur-panel');
124
+ });
125
+ });
126
+ </script>
127
+
128
+ <!--
129
+ Bits UI Tabs, rendered through `child` snippets so every element is this
130
+ component's markup and its styles stay scoped. Extra attributes, `class`
131
+ and Bits UI root props (`activationMode`, `loop`, `disabled`,
132
+ `onValueChange`) go to the root `div.hp-tabs`. Parts: `.hp-tabs-list`
133
+ (role tablist), `.hp-tabs-tab` (role tab, with Bits UI's `data-state` and
134
+ `data-value`), `.hp-tabs-panel` (role tabpanel).
135
+ -->
136
+ <BitsTabs.Root bind:value bind:ref onValueChange={changed} {...rest}>
137
+ {#snippet child({ props })}
138
+ <div {...props} class={['hp-tabs', className]}>
139
+ <BitsTabs.List bind:ref={list} aria-label={label}>
140
+ {#snippet child({ props })}
141
+ <div
142
+ {...props}
143
+ class="hp-tabs-list"
144
+ onpointerdowncapture={() => (input = 'pointer')}
145
+ onkeydowncapture={() => (input = 'key')}
146
+ >
147
+ {#each tabs as tab (tab.value)}
148
+ <BitsTabs.Trigger value={tab.value} disabled={tab.disabled}>
149
+ {#snippet child({ props })}
150
+ <button {...props} class="hp-tabs-tab hp-label hp-focus-inset">{tab.label}</button>
151
+ {/snippet}
152
+ </BitsTabs.Trigger>
153
+ {/each}
154
+ </div>
155
+ {/snippet}
156
+ </BitsTabs.List>
157
+ {#each tabs as tab (tab.value)}
158
+ <BitsTabs.Content value={tab.value}>
159
+ {#snippet child({ props })}
160
+ <div {...props} class="hp-tabs-panel">{@render panel(tab)}</div>
161
+ {/snippet}
162
+ </BitsTabs.Content>
163
+ {/each}
164
+ </div>
165
+ {/snippet}
166
+ </BitsTabs.Root>
167
+
168
+ <style>
169
+ /* In a flex or grid parent the tabs never grow wider than their column:
170
+ the list scrolls instead of widening the page. */
171
+ .hp-tabs {
172
+ min-width: 0;
173
+ }
174
+
175
+ /* One row that scrolls sideways when the tabs do not fit. It reaches
176
+ --bleed past the text edge, as the table band does, and each tab pads
177
+ by --bleed, so the first label keeps the page's text edge. */
178
+ .hp-tabs-list {
179
+ display: flex;
180
+ margin-inline: calc(-1 * var(--bleed));
181
+ overflow-x: auto;
182
+ overscroll-behavior-x: contain;
183
+ /* Scrolling a tab into view leaves part of the next one showing. */
184
+ scroll-padding-inline: var(--space-8);
185
+ /* A thin bar on fine pointers, where a wheel cannot scroll sideways
186
+ without one; touch swipes. */
187
+ scrollbar-width: thin;
188
+ }
189
+ @media (pointer: coarse) {
190
+ .hp-tabs-list {
191
+ scrollbar-width: none;
192
+ }
193
+ }
194
+
195
+ /* The visible box is the hit area: --control-height high (44px on
196
+ coarse pointers) and never narrower than --hit-target. */
197
+ .hp-tabs-tab {
198
+ display: inline-flex;
199
+ flex: none;
200
+ align-items: center;
201
+ justify-content: center;
202
+ box-sizing: border-box;
203
+ min-width: var(--hit-target);
204
+ min-height: var(--control-height);
205
+ padding: 0 var(--bleed);
206
+ border: 0;
207
+ background: transparent;
208
+ color: var(--text);
209
+ white-space: nowrap;
210
+ cursor: pointer;
211
+ -webkit-tap-highlight-color: transparent;
212
+ }
213
+
214
+ /* Selected: a 3px titanium edge on the side facing the panel and the
215
+ selected tint, text unchanged. Selection snaps, so nothing on a tab
216
+ transitions: a fading hover tint would also animate the tab an arrow
217
+ key just left while the pointer rests on it. */
218
+ .hp-tabs-tab[data-state='active'] {
219
+ background: var(--surface-selected);
220
+ box-shadow: inset 0 -3px 0 var(--accent);
221
+ }
222
+
223
+ /* Hover: fine pointers only, never on the selected tab. */
224
+ @media (hover: hover) and (pointer: fine) {
225
+ .hp-tabs-tab[data-state='inactive']:hover:not(:disabled) {
226
+ background: var(--surface-hover);
227
+ }
228
+ }
229
+
230
+ /* Press: one step past hover, so it reads on a phone too. */
231
+ .hp-tabs-tab[data-state='inactive']:active:not(:disabled) {
232
+ background: var(--surface-raised);
233
+ }
234
+
235
+ .hp-tabs-tab:disabled {
236
+ color: var(--muted);
237
+ cursor: not-allowed;
238
+ }
239
+
240
+ /* A panel a pointer chose fades in over --dur-panel (see the script);
241
+ otherwise it cuts. Margin, not padding, keeps the panel's focus
242
+ contour clear of the tabs. */
243
+ .hp-tabs-panel {
244
+ margin-top: var(--space-6);
245
+ }
246
+ </style>
@@ -1,40 +1,40 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import type { HTMLAttributes } from 'svelte/elements';
4
-
5
- /** The elements a tag renders as. Headings for a tag that names a section. */
6
- export type TagElement = 'p' | 'span' | 'div' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
7
-
8
- export type TagProps = HTMLAttributes<HTMLElement> & {
9
- /**
10
- * The element to render: `p` (default), or a heading when the tag names
11
- * a section, with an `id` the section's `aria-labelledby` points at.
12
- */
13
- as?: TagElement;
14
- children?: Snippet;
15
- };
16
- </script>
17
-
18
- <script lang="ts">
19
- let { as = 'p', class: className, children, ...rest }: TagProps = $props();
20
- </script>
21
-
22
- <!--
23
- A small solid titanium block with black caps (BRIEF, visual rule 2). A tag
24
- may sit above a block to name it (da3-51); it sets no outer margin, so the
25
- layout spaces it.
26
- -->
27
- <svelte:element this={as} {...rest} class={['hp-tag', 'hp-label', className]}>{@render children?.()}</svelte:element>
28
-
29
- <style>
30
- /* The caps type comes from .hp-label. The block never stretches to its
31
- container, in block, grid and flex layouts alike. */
32
- .hp-tag {
33
- justify-self: start;
34
- width: fit-content;
35
- /* matches the approved specimen */
36
- padding: 2px 6px;
37
- background: var(--accent);
38
- color: var(--canvas);
39
- }
40
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ /** The elements a tag renders as. Headings for a tag that names a section. */
6
+ export type TagElement = 'p' | 'span' | 'div' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
7
+
8
+ export type TagProps = HTMLAttributes<HTMLElement> & {
9
+ /**
10
+ * The element to render: `p` (default), or a heading when the tag names
11
+ * a section, with an `id` the section's `aria-labelledby` points at.
12
+ */
13
+ as?: TagElement;
14
+ children?: Snippet;
15
+ };
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ let { as = 'p', class: className, children, ...rest }: TagProps = $props();
20
+ </script>
21
+
22
+ <!--
23
+ A small solid titanium block with black caps (BRIEF, visual rule 2). A tag
24
+ may sit above a block to name it (da3-51); it sets no outer margin, so the
25
+ layout spaces it.
26
+ -->
27
+ <svelte:element this={as} {...rest} class={['hp-tag', 'hp-label', className]}>{@render children?.()}</svelte:element>
28
+
29
+ <style>
30
+ /* The caps type comes from .hp-label. The block never stretches to its
31
+ container, in block, grid and flex layouts alike. */
32
+ .hp-tag {
33
+ justify-self: start;
34
+ width: fit-content;
35
+ /* matches the approved specimen */
36
+ padding: 2px 6px;
37
+ background: var(--accent);
38
+ color: var(--canvas);
39
+ }
40
+ </style>