@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,258 +1,275 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
4
-
5
- export type MenuTriggerSnippetProps = {
6
- /** Spread onto the button that opens the menu. */
7
- props: Record<string, unknown>;
8
- };
9
-
10
- /**
11
- * Attributes and handlers not listed here go to the menu element (Bits UI
12
- * `DropdownMenu.Content`), among them `side` (default `bottom`), `align`
13
- * (default `start`), `sideOffset` (default 8), `collisionPadding`
14
- * (default 8), `loop`, `onCloseAutoFocus`, `onEscapeKeydown` and
15
- * `onInteractOutside`. The placement props apply from 600px only; below it
16
- * the menu is a bottom sheet.
17
- */
18
- export type MenuProps = Omit<BitsDropdownMenu.ContentProps, 'child' | 'children' | 'forceMount' | 'ref'> & {
19
- open?: boolean;
20
- /**
21
- * The menu's accessible name, and the sheet's title below 600px. Use the
22
- * trigger's accessible name: its text, or an icon button's `aria-label`.
23
- * A fixed string the app defines, set in caps as the sheet's title, so it
24
- * never carries user data; name a row's item through the trigger's
25
- * `aria-describedby`.
26
- */
27
- label: string;
28
- /** The button the menu opens from: spread `props` onto it. */
29
- trigger: Snippet<[MenuTriggerSnippetProps]>;
30
- /** The items: MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuGroup and MenuSeparator. */
31
- children: Snippet;
32
- /** The sheet's close button's accessible name (below 600px). */
33
- closeLabel?: string;
34
- /** The menu element, `role="menu"`. */
35
- ref?: HTMLElement | null;
36
- };
37
- </script>
38
-
39
- <script lang="ts">
40
- import { DropdownMenu } from 'bits-ui';
41
- import { MediaQuery } from 'svelte/reactivity';
42
- import { pop } from '../motion/index.js';
43
- import Sheet from './internal/Sheet.svelte';
44
-
45
- let {
46
- open = $bindable(false),
47
- label,
48
- trigger,
49
- children,
50
- closeLabel = 'Close',
51
- ref = $bindable(null),
52
- class: className,
53
- onInteractOutside,
54
- onCloseAutoFocus,
55
- side = 'bottom',
56
- align = 'start',
57
- sideOffset = 8,
58
- collisionPadding = 8,
59
- alignOffset,
60
- arrowPadding,
61
- avoidCollisions,
62
- collisionBoundary,
63
- sticky,
64
- hideWhenDetached,
65
- updatePositionStrategy,
66
- strategy,
67
- customAnchor,
68
- ...rest
69
- }: MenuProps = $props();
70
-
71
- // Placement belongs to the menu under the trigger; the sheet always sits
72
- // at the bottom edge, so ContentStatic never receives these.
73
- const placement = $derived({
74
- side,
75
- align,
76
- sideOffset,
77
- collisionPadding,
78
- alignOffset,
79
- arrowPadding,
80
- avoidCollisions,
81
- collisionBoundary,
82
- sticky,
83
- hideWhenDetached,
84
- updatePositionStrategy,
85
- strategy,
86
- customAnchor
87
- });
88
-
89
- // The form follows the width (BRIEF, Apps and phones: Overlays): a menu
90
- // under its trigger from 600px, the shared bottom sheet below it.
91
- const wide = new MediaQuery('min-width: 600px', true);
92
-
93
- // Bits UI moves real focus to the highlighted item, whether keys or a
94
- // pointer moved it. Only a highlight the keys moved draws the focus
95
- // contour; a pointer's draws the tint alone. The items read the contour
96
- // from --menu-contour, which .hp-menu[data-keyboard] sets.
97
- let keyboard = $state(false);
98
-
99
- let sheetNode = $state<HTMLElement | null>(null);
100
- let triggerNode = $state<HTMLElement | null>(null);
101
-
102
- function close() {
103
- open = false;
104
- }
105
-
106
- // The sheet's title row and handle sit outside the menu element; pressing
107
- // them is not an outside press.
108
- function insideSheet(e: PointerEvent) {
109
- if (sheetNode && e.target instanceof Node && sheetNode.contains(e.target)) {
110
- e.preventDefault();
111
- return;
112
- }
113
- onInteractOutside?.(e);
114
- }
115
-
116
- // Bits UI leaves focus where an outside press put it, so a click on
117
- // another control keeps it. Below 600px the only thing outside is the
118
- // scrim, and a press there is a dismiss like the close button, so focus
119
- // returns to the trigger as it does after the close button.
120
- function returnFocus(e: Event) {
121
- onCloseAutoFocus?.(e);
122
- if (e.defaultPrevented) return;
123
- e.preventDefault();
124
- triggerNode?.focus();
125
- }
126
-
127
- // Listens on the menu element without replacing Bits UI's own handlers
128
- // in `props`.
129
- function trackInput(node: HTMLElement) {
130
- const keys = () => (keyboard = true);
131
- const pointer = () => (keyboard = false);
132
- node.addEventListener('keydown', keys, true);
133
- node.addEventListener('pointerdown', pointer, true);
134
- node.addEventListener('pointermove', pointer, true);
135
- return () => {
136
- node.removeEventListener('keydown', keys, true);
137
- node.removeEventListener('pointerdown', pointer, true);
138
- node.removeEventListener('pointermove', pointer, true);
139
- };
140
- }
141
- </script>
142
-
143
- <!--
144
- Bits UI gives the trigger aria-haspopup="menu", aria-expanded and
145
- aria-controls; the menu element takes role="menu" and `label` as its
146
- name. Opened by keys, focus moves to the first item; opened by a pointer,
147
- to the menu itself. Arrow keys move the highlight and loop, letters jump
148
- by typeahead, Enter or Space runs the item. Escape, choosing an item and,
149
- below 600px, the close button, a swipe down or a press on the scrim close
150
- it and return focus to the trigger. From 600px a press outside closes it
151
- and leaves focus where the press put it; Tab closes it and moves on.
152
- -->
153
- <DropdownMenu.Root bind:open>
154
- <DropdownMenu.Trigger
155
- bind:ref={triggerNode}
156
- onkeydowncapture={() => (keyboard = true)}
157
- onpointerdowncapture={() => (keyboard = false)}
158
- >
159
- {#snippet child({ props })}
160
- {@render trigger({ props })}
161
- {/snippet}
162
- </DropdownMenu.Trigger>
163
-
164
- <DropdownMenu.Portal>
165
- {#if wide.current}
166
- <DropdownMenu.Content forceMount bind:ref {...placement} {onInteractOutside} {onCloseAutoFocus} {...rest}>
167
- {#snippet child({ wrapperProps, props, open: shown })}
168
- {#if shown}
169
- <div {...wrapperProps}>
170
- <div
171
- {...props}
172
- class={['hp-menu', className]}
173
- data-form="popover"
174
- data-keyboard={keyboard || undefined}
175
- aria-label={label}
176
- transition:pop
177
- {@attach trackInput}
178
- >
179
- {@render children()}
180
- </div>
181
- </div>
182
- {/if}
183
- {/snippet}
184
- </DropdownMenu.Content>
185
- {:else}
186
- <DropdownMenu.ContentStatic
187
- forceMount
188
- bind:ref
189
- {...rest}
190
- preventScroll
191
- onInteractOutside={insideSheet}
192
- onCloseAutoFocus={returnFocus}
193
- >
194
- {#snippet child({ props, open: shown })}
195
- <Sheet open={shown} layer="popover" title={label} {closeLabel} onclose={close} bind:ref={sheetNode}>
196
- <div
197
- {...props}
198
- class={['hp-menu', className]}
199
- data-form="sheet"
200
- data-keyboard={keyboard || undefined}
201
- aria-label={label}
202
- {@attach trackInput}
203
- >
204
- {@render children()}
205
- </div>
206
- </Sheet>
207
- {/snippet}
208
- </DropdownMenu.ContentStatic>
209
- {/if}
210
- </DropdownMenu.Portal>
211
- </DropdownMenu.Root>
212
-
213
- <style>
214
- /* The list in both forms. Items read two inherited properties:
215
- --menu-inset, the row's side padding, so item text lines up with the
216
- menu's edge here and with the sheet's title there; and --menu-contour,
217
- the outline a highlighted row draws, which only keys turn on. The menu
218
- element takes focus when a pointer opens it; the rows show where focus
219
- is, so the menu itself draws no contour. */
220
- .hp-menu {
221
- --menu-inset: var(--space-3);
222
- --menu-contour: none;
223
-
224
- box-sizing: border-box;
225
- padding-block: var(--space-1);
226
- background: var(--canvas);
227
- color: var(--text);
228
- font-family: var(--sans);
229
- outline: none;
230
- }
231
- .hp-menu[data-keyboard] {
232
- --menu-contour: 2px solid var(--focus);
233
- }
234
-
235
- /* ---- Popover (600px and up): a black panel under its trigger --------- */
236
-
237
- /* On the popover layer (tokens.css), over the shell and over an open
238
- dialog; Bits UI copies the z-index to the positioned wrapper. At least
239
- as wide as the trigger and 192px, at most 320px, and as tall as the
240
- space beside the trigger; a longer list scrolls inside. */
241
- .hp-menu[data-form='popover'] {
242
- position: relative;
243
- z-index: var(--layer-popover);
244
- width: max-content;
245
- min-width: max(var(--bits-dropdown-menu-anchor-width), 192px);
246
- max-width: min(320px, var(--bits-dropdown-menu-content-available-width));
247
- max-height: var(--bits-dropdown-menu-content-available-height);
248
- overflow-y: auto;
249
- overscroll-behavior: contain;
250
- border: 1px solid var(--border-control);
251
- }
252
-
253
- /* ---- Sheet (compact): the list is the sheet's body, which scrolls ---- */
254
-
255
- .hp-menu[data-form='sheet'] {
256
- --menu-inset: var(--gutter);
257
- }
258
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { DropdownMenu as BitsDropdownMenu } from 'bits-ui';
4
+
5
+ export type MenuTriggerSnippetProps = {
6
+ /** Spread onto the button that opens the menu. */
7
+ props: Record<string, unknown>;
8
+ };
9
+
10
+ /**
11
+ * Attributes and handlers not listed here go to the menu element (Bits UI
12
+ * `DropdownMenu.Content`), among them `side` (default `bottom`), `align`
13
+ * (default `start`), `sideOffset` (default 8), `collisionPadding`
14
+ * (default 8), `loop`, `onCloseAutoFocus`, `onEscapeKeydown` and
15
+ * `onInteractOutside`. The placement props apply from 600px only; below it
16
+ * the menu is a bottom sheet.
17
+ */
18
+ export type MenuProps = Omit<BitsDropdownMenu.ContentProps, 'child' | 'children' | 'forceMount' | 'ref'> & {
19
+ open?: boolean;
20
+ /**
21
+ * The menu's accessible name, and the sheet's title below 600px. Use the
22
+ * trigger's accessible name: its text, or an icon button's `aria-label`.
23
+ * A fixed string the app defines, set in caps as the sheet's title, so it
24
+ * never carries user data; name a row's item through the trigger's
25
+ * `aria-describedby`.
26
+ */
27
+ label: string;
28
+ /** The button the menu opens from: spread `props` onto it. */
29
+ trigger: Snippet<[MenuTriggerSnippetProps]>;
30
+ /** The items: MenuItem, MenuCheckboxItem, MenuRadioGroup, MenuGroup and MenuSeparator. */
31
+ children: Snippet;
32
+ /** The sheet's close button's accessible name (below 600px). */
33
+ closeLabel?: string;
34
+ /** The menu element, `role="menu"`. */
35
+ ref?: HTMLElement | null;
36
+ };
37
+ </script>
38
+
39
+ <script lang="ts">
40
+ import { tick } from 'svelte';
41
+ import { DropdownMenu } from 'bits-ui';
42
+ import { MediaQuery } from 'svelte/reactivity';
43
+ import { pop } from '../motion/index.js';
44
+ import Sheet from './internal/Sheet.svelte';
45
+
46
+ let {
47
+ open = $bindable(false),
48
+ label,
49
+ trigger,
50
+ children,
51
+ closeLabel = 'Close',
52
+ ref = $bindable(null),
53
+ class: className,
54
+ onInteractOutside,
55
+ onCloseAutoFocus,
56
+ side = 'bottom',
57
+ align = 'start',
58
+ sideOffset = 8,
59
+ collisionPadding = 8,
60
+ alignOffset,
61
+ arrowPadding,
62
+ avoidCollisions,
63
+ collisionBoundary,
64
+ sticky,
65
+ hideWhenDetached,
66
+ updatePositionStrategy,
67
+ strategy,
68
+ customAnchor,
69
+ ...rest
70
+ }: MenuProps = $props();
71
+
72
+ // Placement belongs to the menu under the trigger; the sheet always sits
73
+ // at the bottom edge, so ContentStatic never receives these.
74
+ const placement = $derived({
75
+ side,
76
+ align,
77
+ sideOffset,
78
+ collisionPadding,
79
+ alignOffset,
80
+ arrowPadding,
81
+ avoidCollisions,
82
+ collisionBoundary,
83
+ sticky,
84
+ hideWhenDetached,
85
+ updatePositionStrategy,
86
+ strategy,
87
+ customAnchor
88
+ });
89
+
90
+ // The form follows the width (BRIEF, Apps and phones: Overlays): a menu
91
+ // under its trigger from 600px, the shared bottom sheet below it.
92
+ const wide = new MediaQuery('min-width: 600px', true);
93
+
94
+ // Bits UI moves real focus to the highlighted item, whether keys or a
95
+ // pointer moved it. Only a highlight the keys moved draws the focus
96
+ // contour; a pointer's draws the tint alone. The items read the contour
97
+ // from --menu-contour, which .hp-menu[data-keyboard] sets.
98
+ let keyboard = $state(false);
99
+
100
+ let sheetNode = $state<HTMLElement | null>(null);
101
+ let triggerNode = $state<HTMLElement | null>(null);
102
+
103
+ // Bits UI's content layers (the portal, positioning, focus and dismiss
104
+ // handling) cost about 0.3 ms a menu, and an app may draw several closed
105
+ // menus per view, so they mount on the first open, not with the
106
+ // trigger. Until then only the trigger and its ARIA exist. They mount
107
+ // closed, and Bits UI sees the menu open one update later, so the first
108
+ // entrance plays as every later one does. They stay mounted after that.
109
+ let layers = $state(false);
110
+ let ready = $state(false);
111
+ $effect(() => {
112
+ if (!open || layers) return;
113
+ layers = true;
114
+ tick().then(() => (ready = true));
115
+ });
116
+
117
+ function close() {
118
+ open = false;
119
+ }
120
+
121
+ // The sheet's title row and handle sit outside the menu element; pressing
122
+ // them is not an outside press.
123
+ function insideSheet(e: PointerEvent) {
124
+ if (sheetNode && e.target instanceof Node && sheetNode.contains(e.target)) {
125
+ e.preventDefault();
126
+ return;
127
+ }
128
+ onInteractOutside?.(e);
129
+ }
130
+
131
+ // Bits UI leaves focus where an outside press put it, so a click on
132
+ // another control keeps it. Below 600px the only thing outside is the
133
+ // scrim, and a press there is a dismiss like the close button, so focus
134
+ // returns to the trigger as it does after the close button.
135
+ function returnFocus(e: Event) {
136
+ onCloseAutoFocus?.(e);
137
+ if (e.defaultPrevented) return;
138
+ e.preventDefault();
139
+ triggerNode?.focus();
140
+ }
141
+
142
+ // Listens on the menu element without replacing Bits UI's own handlers
143
+ // in `props`.
144
+ function trackInput(node: HTMLElement) {
145
+ const keys = () => (keyboard = true);
146
+ const pointer = () => (keyboard = false);
147
+ node.addEventListener('keydown', keys, true);
148
+ node.addEventListener('pointerdown', pointer, true);
149
+ node.addEventListener('pointermove', pointer, true);
150
+ return () => {
151
+ node.removeEventListener('keydown', keys, true);
152
+ node.removeEventListener('pointerdown', pointer, true);
153
+ node.removeEventListener('pointermove', pointer, true);
154
+ };
155
+ }
156
+ </script>
157
+
158
+ <!--
159
+ Bits UI gives the trigger aria-haspopup="menu", aria-expanded and
160
+ aria-controls; the menu element takes role="menu" and `label` as its
161
+ name. Opened by keys, focus moves to the first item; opened by a pointer,
162
+ to the menu itself. Arrow keys move the highlight and loop, letters jump
163
+ by typeahead, Enter or Space runs the item. Escape, choosing an item and,
164
+ below 600px, the close button, a swipe down or a press on the scrim close
165
+ it and return focus to the trigger. From 600px a press outside closes it
166
+ and leaves focus where the press put it; Tab closes it and moves on.
167
+ -->
168
+ <DropdownMenu.Root bind:open={() => open && ready, (value) => (open = value)}>
169
+ <DropdownMenu.Trigger
170
+ bind:ref={triggerNode}
171
+ onkeydowncapture={() => (keyboard = true)}
172
+ onpointerdowncapture={() => (keyboard = false)}
173
+ >
174
+ {#snippet child({ props })}
175
+ {@render trigger({ props })}
176
+ {/snippet}
177
+ </DropdownMenu.Trigger>
178
+
179
+ {#if layers}
180
+ <DropdownMenu.Portal>
181
+ {#if wide.current}
182
+ <DropdownMenu.Content forceMount bind:ref {...placement} {onInteractOutside} {onCloseAutoFocus} {...rest}>
183
+ {#snippet child({ wrapperProps, props, open: shown })}
184
+ {#if shown}
185
+ <div {...wrapperProps}>
186
+ <div
187
+ {...props}
188
+ class={['hp-menu', className]}
189
+ data-form="popover"
190
+ data-keyboard={keyboard || undefined}
191
+ aria-label={label}
192
+ transition:pop
193
+ {@attach trackInput}
194
+ >
195
+ {@render children()}
196
+ </div>
197
+ </div>
198
+ {/if}
199
+ {/snippet}
200
+ </DropdownMenu.Content>
201
+ {:else}
202
+ <DropdownMenu.ContentStatic
203
+ forceMount
204
+ bind:ref
205
+ {...rest}
206
+ preventScroll
207
+ onInteractOutside={insideSheet}
208
+ onCloseAutoFocus={returnFocus}
209
+ >
210
+ {#snippet child({ props, open: shown })}
211
+ <Sheet open={shown} layer="popover" title={label} {closeLabel} onclose={close} bind:ref={sheetNode}>
212
+ <div
213
+ {...props}
214
+ class={['hp-menu', className]}
215
+ data-form="sheet"
216
+ data-keyboard={keyboard || undefined}
217
+ aria-label={label}
218
+ {@attach trackInput}
219
+ >
220
+ {@render children()}
221
+ </div>
222
+ </Sheet>
223
+ {/snippet}
224
+ </DropdownMenu.ContentStatic>
225
+ {/if}
226
+ </DropdownMenu.Portal>
227
+ {/if}
228
+ </DropdownMenu.Root>
229
+
230
+ <style>
231
+ /* The list in both forms. Items read two inherited properties:
232
+ --menu-inset, the row's side padding, so item text lines up with the
233
+ menu's edge here and with the sheet's title there; and --menu-contour,
234
+ the outline a highlighted row draws, which only keys turn on. The menu
235
+ element takes focus when a pointer opens it; the rows show where focus
236
+ is, so the menu itself draws no contour. */
237
+ .hp-menu {
238
+ --menu-inset: var(--space-3);
239
+ --menu-contour: none;
240
+
241
+ box-sizing: border-box;
242
+ padding-block: var(--space-1);
243
+ background: var(--canvas);
244
+ color: var(--text);
245
+ font-family: var(--sans);
246
+ outline: none;
247
+ }
248
+ .hp-menu[data-keyboard] {
249
+ --menu-contour: 2px solid var(--focus);
250
+ }
251
+
252
+ /* ---- Popover (600px and up): a black panel under its trigger --------- */
253
+
254
+ /* On the popover layer (tokens.css), over the shell and over an open
255
+ dialog; Bits UI copies the z-index to the positioned wrapper. At least
256
+ as wide as the trigger and 192px, at most 320px, and as tall as the
257
+ space beside the trigger; a longer list scrolls inside. */
258
+ .hp-menu[data-form='popover'] {
259
+ position: relative;
260
+ z-index: var(--layer-popover);
261
+ width: max-content;
262
+ min-width: max(var(--bits-dropdown-menu-anchor-width), 192px);
263
+ max-width: min(320px, var(--bits-dropdown-menu-content-available-width));
264
+ max-height: var(--bits-dropdown-menu-content-available-height);
265
+ overflow-y: auto;
266
+ overscroll-behavior: contain;
267
+ border: 1px solid var(--border-control);
268
+ }
269
+
270
+ /* ---- Sheet (compact): the list is the sheet's body, which scrolls ---- */
271
+
272
+ .hp-menu[data-form='sheet'] {
273
+ --menu-inset: var(--gutter);
274
+ }
275
+ </style>