@mystravil/hardpoint 0.1.0 → 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 (36) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +5 -5
  3. package/dist/base.css +279 -0
  4. package/dist/components/Checkbox.svelte +46 -9
  5. package/dist/components/Checkbox.svelte.d.ts +9 -2
  6. package/dist/components/CheckboxGroup.svelte +131 -23
  7. package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
  8. package/dist/components/Combobox.svelte +104 -27
  9. package/dist/components/Combobox.svelte.d.ts +10 -1
  10. package/dist/components/Dialog.svelte +4 -1
  11. package/dist/components/ImageViewer.svelte +764 -0
  12. package/dist/components/ImageViewer.svelte.d.ts +55 -0
  13. package/dist/components/ListDetail.svelte +218 -0
  14. package/dist/components/ListDetail.svelte.d.ts +37 -0
  15. package/dist/components/Meter.svelte +283 -0
  16. package/dist/components/Meter.svelte.d.ts +66 -0
  17. package/dist/components/NativeSelect.svelte +135 -119
  18. package/dist/components/NativeSelect.svelte.d.ts +6 -0
  19. package/dist/components/Notice.svelte +262 -0
  20. package/dist/components/Notice.svelte.d.ts +30 -0
  21. package/dist/components/Popover.svelte +6 -6
  22. package/dist/components/RadioGroup.svelte +440 -0
  23. package/dist/components/RadioGroup.svelte.d.ts +50 -0
  24. package/dist/components/Select.svelte +89 -23
  25. package/dist/components/Select.svelte.d.ts +6 -0
  26. package/dist/components/Spinner.svelte +3 -2
  27. package/dist/components/TextArea.svelte +272 -0
  28. package/dist/components/TextArea.svelte.d.ts +28 -0
  29. package/dist/components/Toaster.svelte +6 -6
  30. package/dist/components/Tooltip.svelte +5 -5
  31. package/dist/components/internal/groups.d.ts +14 -0
  32. package/dist/components/internal/groups.js +23 -0
  33. package/dist/index.d.ts +6 -0
  34. package/dist/index.js +6 -0
  35. package/dist/tokens.css +4 -0
  36. package/package.json +1 -1
@@ -0,0 +1,272 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
3
+
4
+ /** Attributes and handlers not listed here go to the `<textarea>`. */
5
+ export type TextAreaProps = Omit<HTMLTextareaAttributes, 'value' | 'children' | 'rows'> & {
6
+ /** The caps field label above the well. Required: a placeholder is never the only label. */
7
+ label: string;
8
+ /** The text. Bindable. */
9
+ value?: string;
10
+ /** Guidance under the well in `--muted`, linked by `aria-describedby`. */
11
+ hint?: string;
12
+ /**
13
+ * The error to show under the well, with an icon, linked by
14
+ * `aria-describedby`; it sets `aria-invalid`. Say how to fix the
15
+ * problem. The app validates; the field shows what it is given.
16
+ */
17
+ error?: string;
18
+ /** The height of the empty well, in lines. */
19
+ rows?: number;
20
+ /**
21
+ * The well grows with its text up to this many lines, then scrolls
22
+ * inside. Never less than `rows`.
23
+ */
24
+ maxRows?: number;
25
+ /** The `<textarea>`, for example to focus the first error on submit. */
26
+ ref?: HTMLTextAreaElement | null;
27
+ };
28
+ </script>
29
+
30
+ <script lang="ts">
31
+ import { untrack } from 'svelte';
32
+ import { Label } from 'bits-ui';
33
+ import { fadeIn } from './internal/fade-in.js';
34
+
35
+ const uid = $props.id();
36
+
37
+ let {
38
+ label,
39
+ value = $bindable(''),
40
+ hint,
41
+ error,
42
+ rows = 3,
43
+ maxRows = 10,
44
+ id = uid,
45
+ disabled,
46
+ readonly,
47
+ ref = $bindable(null),
48
+ class: className,
49
+ 'aria-describedby': describedBy,
50
+ 'aria-invalid': ariaInvalid,
51
+ ...rest
52
+ }: TextAreaProps = $props();
53
+
54
+ const hintId = $derived(`${id}-hint`);
55
+ const errorId = $derived(`${id}-error`);
56
+ // The error first, so a screen reader reads it before the hint.
57
+ const describedByIds = $derived(
58
+ [error ? errorId : undefined, hint ? hintId : undefined, describedBy].filter(Boolean).join(' ') ||
59
+ undefined
60
+ );
61
+
62
+ // ---- Short motion: the error (BRIEF, --dur-message) --------------------
63
+
64
+ // As TextField: an error that appears after the first render fades in
65
+ // from 0 over --dur-message, and the well's edge fades to --danger with
66
+ // it (the CSS below). An error that clears goes at once, and so does the
67
+ // red edge. A field that renders or mounts with an error shows it at once.
68
+ let root: HTMLDivElement | null = $state(null);
69
+ let errorShown = untrack(() => !!error);
70
+ $effect(() => {
71
+ const shown = !!error;
72
+ untrack(() => {
73
+ const appeared = shown && !errorShown;
74
+ errorShown = shown;
75
+ const message = appeared ? root?.querySelector(':scope > .error') : null;
76
+ if (message) fadeIn(message, '--dur-message');
77
+ });
78
+ });
79
+
80
+ // ---- Growth ------------------------------------------------------------
81
+
82
+ // Where the browser has field-sizing (Chromium 123+), CSS alone sizes the
83
+ // well between its rows and maxRows heights. Elsewhere the height comes
84
+ // from scrollHeight whenever the text, the line counts or the width
85
+ // change, written through element.style, never a style attribute.
86
+ const sizesItself = typeof CSS !== 'undefined' && CSS.supports('field-sizing', 'content');
87
+
88
+ function fit(field: HTMLTextAreaElement) {
89
+ // The well collapses to its content to measure it. Holding the root's
90
+ // height meanwhile keeps the page from getting shorter, which would
91
+ // move its scroll position. Without a scrollbar while it measures, the
92
+ // text wraps at the width it has once it fits.
93
+ if (root) root.style.minHeight = `${root.offsetHeight}px`;
94
+ field.style.overflowY = 'hidden';
95
+ field.style.height = 'auto';
96
+ const borders = field.offsetHeight - field.clientHeight;
97
+ field.style.height = `${field.scrollHeight + borders}px`;
98
+ field.style.overflowY = '';
99
+ if (root) root.style.minHeight = '';
100
+ }
101
+
102
+ $effect(() => {
103
+ const field = ref;
104
+ if (sizesItself || !field) return;
105
+ // Read so the effect runs again when they change. A new `style` from
106
+ // the app replaces the element's inline height, so it refits too.
107
+ void [value, rows, maxRows, rest.style];
108
+ fit(field);
109
+ });
110
+
111
+ $effect(() => {
112
+ const field = ref;
113
+ if (sizesItself || !field) return;
114
+ let width = field.clientWidth;
115
+ const observer = new ResizeObserver(() => {
116
+ if (field.clientWidth === width) return;
117
+ width = field.clientWidth;
118
+ fit(field);
119
+ });
120
+ observer.observe(field);
121
+ return () => observer.disconnect();
122
+ });
123
+ </script>
124
+
125
+ <!--
126
+ Multi-line text in the black well, with a plain caps label above it (Bits
127
+ UI Label), an optional hint and an inline error below. The well starts
128
+ `rows` lines high, grows with its text up to `maxRows` lines, then scrolls
129
+ inside. Extra attributes (`name`, `placeholder`, `required`, `maxlength`,
130
+ `autocomplete`, `enterkeyhint`), handlers and `class` go to the
131
+ <textarea>. The textarea is never re-created and its value is written
132
+ only when it differs, so the text, the caret and focus survive any
133
+ re-render around it.
134
+ -->
135
+ <div
136
+ bind:this={root}
137
+ class="hp-text-area"
138
+ data-invalid={error ? '' : undefined}
139
+ data-disabled={disabled ? '' : undefined}
140
+ data-readonly={readonly ? '' : undefined}
141
+ style:--text-area-rows={Math.max(1, rows)}
142
+ style:--text-area-max-rows={Math.max(1, maxRows)}
143
+ >
144
+ <Label.Root for={id}>
145
+ {#snippet child({ props })}
146
+ <label {...props} class="hp-label">{label}</label>
147
+ {/snippet}
148
+ </Label.Root>
149
+ <textarea
150
+ {...rest}
151
+ bind:this={ref}
152
+ bind:value
153
+ {id}
154
+ rows={Math.max(1, rows)}
155
+ {disabled}
156
+ {readonly}
157
+ class={['hp-well', className]}
158
+ aria-describedby={describedByIds}
159
+ aria-invalid={error ? true : ariaInvalid}
160
+ ></textarea>
161
+ {#if hint}
162
+ <p class="hint hp-caption" id={hintId}>{hint}</p>
163
+ {/if}
164
+ {#if error}
165
+ <p class="error hp-caption" id={errorId}>
166
+ <span class="icon" aria-hidden="true">
167
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
168
+ <path d="M1.5 1.5h13v13h-13z" />
169
+ <path d="M8 4v4M8 10v2" stroke-width="2" />
170
+ </svg>
171
+ </span>
172
+ <span>{error}</span>
173
+ </p>
174
+ {/if}
175
+ </div>
176
+
177
+ <style>
178
+ .hp-text-area {
179
+ display: grid;
180
+ gap: var(--space-1);
181
+ min-width: 0;
182
+ }
183
+ label {
184
+ color: var(--muted);
185
+ }
186
+
187
+ /* The well, in lines of 24px. Its heights are the line count times the
188
+ line, plus 12px of padding at the top and bottom and the two 1px
189
+ borders: 98px empty and 266px at most by default. The root sets the
190
+ line counts from `rows` and `maxRows`; the fallbacks are the defaults,
191
+ for a page whose policy blocks the style attribute of server-rendered
192
+ markup. The text is 16px at every width, from .hp-well. */
193
+ textarea {
194
+ display: block;
195
+ width: 100%;
196
+ min-width: 0;
197
+ height: auto;
198
+ min-height: calc(var(--text-area-rows, 3) * 24px + 2 * var(--space-3) + 2px);
199
+ max-height: calc(var(--text-area-max-rows, 10) * 24px + 2 * var(--space-3) + 2px);
200
+ padding: var(--space-3);
201
+ line-height: 24px;
202
+ field-sizing: content;
203
+ resize: none;
204
+ overflow-y: auto;
205
+ /* Once it scrolls, the browser keeps the caret's line clear of the
206
+ padding, so the line being typed never sits on the edge. */
207
+ scroll-padding-block: var(--space-3);
208
+ /* As the other scrolling parts: a thin bar on fine pointers; touch
209
+ shows its own while it scrolls. */
210
+ scrollbar-width: thin;
211
+ }
212
+ textarea::placeholder {
213
+ color: var(--muted);
214
+ opacity: 1;
215
+ }
216
+
217
+ /* Read-only: muted text as when disabled, and no hover fill, because
218
+ nothing here can change. It keeps the 3:1 boundary: the field still
219
+ takes focus and its text can be selected and copied, so it stays a
220
+ control (WCAG 1.4.11 exempts only inactive ones). */
221
+ textarea[readonly] {
222
+ color: var(--muted);
223
+ }
224
+ @media (hover: hover) and (pointer: fine) {
225
+ textarea[readonly]:hover {
226
+ background-color: var(--canvas);
227
+ }
228
+ }
229
+
230
+ /* Invalid: as TextField. The boundary turns --danger, never alone: the
231
+ icon and the message say what is wrong. Keyed on the root's
232
+ data-invalid, set by `error`. The edge fades to --danger over
233
+ --dur-message with the message and goes back at once when the error
234
+ clears. A disabled well keeps --border. */
235
+ [data-invalid] textarea {
236
+ border-color: var(--danger);
237
+ transition:
238
+ background-color var(--dur-hover) var(--settle),
239
+ border-color var(--dur-message) var(--settle);
240
+ }
241
+ [data-invalid] textarea:disabled {
242
+ border-color: var(--border);
243
+ }
244
+
245
+ .hint {
246
+ margin: 0;
247
+ color: var(--muted);
248
+ }
249
+ .error {
250
+ display: flex;
251
+ gap: var(--space-2);
252
+ margin: 0;
253
+ color: var(--danger);
254
+ }
255
+ /* The first message starts 8px under the well, clear of the focus
256
+ contour (2px, drawn 4px outside the well). */
257
+ textarea + .hint,
258
+ textarea + .error {
259
+ margin-top: var(--space-1);
260
+ }
261
+ /* One line box high, so the icon centres on the first line of the
262
+ message however it wraps. */
263
+ .icon {
264
+ display: flex;
265
+ flex: none;
266
+ align-items: center;
267
+ height: 1lh;
268
+ }
269
+ .icon svg {
270
+ display: block;
271
+ }
272
+ </style>
@@ -0,0 +1,28 @@
1
+ import type { HTMLTextareaAttributes } from 'svelte/elements';
2
+ /** Attributes and handlers not listed here go to the `<textarea>`. */
3
+ export type TextAreaProps = Omit<HTMLTextareaAttributes, 'value' | 'children' | 'rows'> & {
4
+ /** The caps field label above the well. Required: a placeholder is never the only label. */
5
+ label: string;
6
+ /** The text. Bindable. */
7
+ value?: string;
8
+ /** Guidance under the well in `--muted`, linked by `aria-describedby`. */
9
+ hint?: string;
10
+ /**
11
+ * The error to show under the well, with an icon, linked by
12
+ * `aria-describedby`; it sets `aria-invalid`. Say how to fix the
13
+ * problem. The app validates; the field shows what it is given.
14
+ */
15
+ error?: string;
16
+ /** The height of the empty well, in lines. */
17
+ rows?: number;
18
+ /**
19
+ * The well grows with its text up to this many lines, then scrolls
20
+ * inside. Never less than `rows`.
21
+ */
22
+ maxRows?: number;
23
+ /** The `<textarea>`, for example to focus the first error on submit. */
24
+ ref?: HTMLTextAreaElement | null;
25
+ };
26
+ declare const TextArea: import("svelte").Component<TextAreaProps, {}, "value" | "ref">;
27
+ type TextArea = ReturnType<typeof TextArea>;
28
+ export default TextArea;
@@ -341,16 +341,16 @@
341
341
 
342
342
  <style>
343
343
  /* The Tooltip's box at the bottom centre: black, the control boundary,
344
- square. As wide as its text up to --toast-width (an app passes
345
- style="--toast-width: …" for another cap), inside the page gutters and
346
- the safe areas, so it never meets a screen edge. Bits UI sets
344
+ square. As wide as its text up to --toast-width, 30rem unless set (an
345
+ app sets it with a class on the Toaster and a rule in its stylesheet;
346
+ the default is the var() fallback, so any rule wins whatever its
347
+ specificity), inside the page gutters and the safe areas, so it never
348
+ meets a screen edge. Bits UI sets
347
349
  pointer-events: none on <body> while a dialog or a list is open; the
348
350
  toast takes presses anyway. A drag along either axis is the swipe's,
349
351
  so it never scrolls the page under the toast; pinch zoom stays the
350
352
  browser's. */
351
353
  .hp-toaster {
352
- --toast-width: 30rem;
353
-
354
354
  position: fixed;
355
355
  right: calc(var(--gutter) + env(safe-area-inset-right));
356
356
  bottom: calc(var(--gutter) + env(safe-area-inset-bottom));
@@ -360,7 +360,7 @@
360
360
  align-items: center;
361
361
  box-sizing: border-box;
362
362
  width: fit-content;
363
- max-width: var(--toast-width);
363
+ max-width: var(--toast-width, 30rem);
364
364
  margin: 0 auto;
365
365
  border: 1px solid var(--border-control);
366
366
  background: var(--canvas);
@@ -169,16 +169,16 @@
169
169
  /* On the tooltip layer (tokens.css), over popovers, menus and dialogs.
170
170
  Bits UI copies the z-index to the positioned wrapper. A black box with
171
171
  the control boundary, like the popover, as wide as its text up to
172
- --tooltip-width (an app passes style="--tooltip-width: …" for another
173
- cap) and never wider than the space beside the trigger. */
172
+ --tooltip-width, 280px unless set (an app sets it with a class on the
173
+ Tooltip and a rule in its stylesheet; the default is the var()
174
+ fallback, so any rule wins whatever its specificity) and never wider
175
+ than the space beside the trigger. */
174
176
  .hp-tooltip {
175
- --tooltip-width: 280px;
176
-
177
177
  position: relative;
178
178
  z-index: var(--layer-tooltip);
179
179
  box-sizing: border-box;
180
180
  width: max-content;
181
- max-width: min(var(--tooltip-width), var(--bits-tooltip-content-available-width));
181
+ max-width: min(var(--tooltip-width, 280px), var(--bits-tooltip-content-available-width));
182
182
  padding: var(--space-1) var(--space-2);
183
183
  border: 1px solid var(--border-control);
184
184
  background: var(--canvas);
@@ -0,0 +1,14 @@
1
+ export type OptionGroup<T> = {
2
+ name: string;
3
+ items: T[];
4
+ };
5
+ export type GroupedOptions<T> = {
6
+ /** The items without a group, in their order. */
7
+ loose: T[];
8
+ groups: OptionGroup<T>[];
9
+ /** Every item in the order the list shows them. */
10
+ ordered: T[];
11
+ };
12
+ export declare function groupOptions<T extends {
13
+ group?: string;
14
+ }>(items: readonly T[]): GroupedOptions<T>;
@@ -0,0 +1,23 @@
1
+ // Options in named groups, in one list (Select, Combobox, NativeSelect).
2
+ // Items without a group come first, before any heading; then each group,
3
+ // in the order its first item appears. Items keep their order inside a
4
+ // group. An empty `group` counts as none, because a heading needs text.
5
+ export function groupOptions(items) {
6
+ const loose = [];
7
+ const byName = new Map();
8
+ for (const item of items) {
9
+ if (!item.group) {
10
+ loose.push(item);
11
+ continue;
12
+ }
13
+ const members = byName.get(item.group);
14
+ if (members)
15
+ members.push(item);
16
+ else
17
+ byName.set(item.group, [item]);
18
+ }
19
+ if (!byName.size)
20
+ return { loose, groups: [], ordered: loose };
21
+ const groups = Array.from(byName, ([name, members]) => ({ name, items: members }));
22
+ return { loose, groups, ordered: loose.concat(...groups.map((g) => g.items)) };
23
+ }
package/dist/index.d.ts CHANGED
@@ -4,25 +4,31 @@ export { default as Pagination, type PaginationProps } from './components/Pagina
4
4
  export { default as Select, type SelectItem, type SelectItemSnippetProps, type SelectLabelBeside, type SelectProps } from './components/Select.svelte';
5
5
  export { default as Table, type Column, type TableBreakpoint, type TableMark, type TableStack, type TableVariant } from './components/Table.svelte';
6
6
  export { default as AppShell, type AppShellCompactNav, type AppShellDestination, type AppShellProps, type AppShellWideNav } from './components/AppShell.svelte';
7
+ export { default as ListDetail, type ListDetailBreakpoint, type ListDetailProps, type ListDetailSnippetProps } from './components/ListDetail.svelte';
7
8
  export { default as Dialog, type DialogActionsSnippetProps, type DialogProps, type DialogTriggerSnippetProps } from './components/Dialog.svelte';
8
9
  export { default as Tag, type TagElement, type TagProps } from './components/Tag.svelte';
9
10
  export { default as KeyValueList, type KeyValueItem, type KeyValueListProps } from './components/KeyValueList.svelte';
10
11
  export { default as Timeline, type TimelineEvent, type TimelineProps, type TimelineTone } from './components/Timeline.svelte';
11
12
  export { default as MediaFrame, type MediaFrameProps } from './components/MediaFrame.svelte';
12
13
  export { default as MediaImage, type MediaImageProps } from './components/MediaImage.svelte';
14
+ export { default as ImageViewer, type ImageViewerProps, type ImageViewerTriggerSnippetProps } from './components/ImageViewer.svelte';
13
15
  export { default as TextField, type TextFieldBreakpoint, type TextFieldProps, type TextFieldType } from './components/TextField.svelte';
16
+ export { default as TextArea, type TextAreaProps } from './components/TextArea.svelte';
14
17
  export { default as Tabs, type TabsItem, type TabsProps } from './components/Tabs.svelte';
15
18
  export { default as Checkbox, type CheckboxProps } from './components/Checkbox.svelte';
16
19
  export { default as CheckboxGroup, type CheckboxGroupItem, type CheckboxGroupProps } from './components/CheckboxGroup.svelte';
20
+ export { default as RadioGroup, type RadioGroupItem, type RadioGroupProps } from './components/RadioGroup.svelte';
17
21
  export { default as Switch, type SwitchProps } from './components/Switch.svelte';
18
22
  export { default as Stat, type StatProps, type StatTone, type StatVariant } from './components/Stat.svelte';
19
23
  export { default as Steps, type StepsBreakpoint, type StepsItem, type StepsProps, type StepsState } from './components/Steps.svelte';
20
24
  export { default as Progress, type ProgressProps } from './components/Progress.svelte';
25
+ export { default as Meter, type MeterProps, type MeterTone } from './components/Meter.svelte';
21
26
  export { default as Spinner, type SpinnerProps } from './components/Spinner.svelte';
22
27
  export { default as Skeleton, type SkeletonProps, type SkeletonVariant } from './components/Skeleton.svelte';
23
28
  export { default as LoadRegion, type LoadRegionProps } from './components/LoadRegion.svelte';
24
29
  export { default as EmptyState, type EmptyStateProps, type EmptyStateTitleElement, type EmptyStateVariant } from './components/EmptyState.svelte';
25
30
  export { default as ErrorState, type ErrorStateProps, type ErrorStateTitleElement, type ErrorStateVariant } from './components/ErrorState.svelte';
31
+ export { default as Notice, type NoticeProps, type NoticeTitleElement, type NoticeTone } from './components/Notice.svelte';
26
32
  export { default as Popover, type PopoverActionsSnippetProps, type PopoverProps, type PopoverTriggerSnippetProps } from './components/Popover.svelte';
27
33
  export { default as Menu, type MenuProps, type MenuTriggerSnippetProps } from './components/Menu.svelte';
28
34
  export { default as MenuItem, type MenuItemProps, type MenuItemVariant } from './components/MenuItem.svelte';
package/dist/index.js CHANGED
@@ -4,25 +4,31 @@ export { default as Pagination } from './components/Pagination.svelte';
4
4
  export { default as Select } from './components/Select.svelte';
5
5
  export { default as Table } from './components/Table.svelte';
6
6
  export { default as AppShell } from './components/AppShell.svelte';
7
+ export { default as ListDetail } from './components/ListDetail.svelte';
7
8
  export { default as Dialog } from './components/Dialog.svelte';
8
9
  export { default as Tag } from './components/Tag.svelte';
9
10
  export { default as KeyValueList } from './components/KeyValueList.svelte';
10
11
  export { default as Timeline } from './components/Timeline.svelte';
11
12
  export { default as MediaFrame } from './components/MediaFrame.svelte';
12
13
  export { default as MediaImage } from './components/MediaImage.svelte';
14
+ export { default as ImageViewer } from './components/ImageViewer.svelte';
13
15
  export { default as TextField } from './components/TextField.svelte';
16
+ export { default as TextArea } from './components/TextArea.svelte';
14
17
  export { default as Tabs } from './components/Tabs.svelte';
15
18
  export { default as Checkbox } from './components/Checkbox.svelte';
16
19
  export { default as CheckboxGroup } from './components/CheckboxGroup.svelte';
20
+ export { default as RadioGroup } from './components/RadioGroup.svelte';
17
21
  export { default as Switch } from './components/Switch.svelte';
18
22
  export { default as Stat } from './components/Stat.svelte';
19
23
  export { default as Steps } from './components/Steps.svelte';
20
24
  export { default as Progress } from './components/Progress.svelte';
25
+ export { default as Meter } from './components/Meter.svelte';
21
26
  export { default as Spinner } from './components/Spinner.svelte';
22
27
  export { default as Skeleton } from './components/Skeleton.svelte';
23
28
  export { default as LoadRegion } from './components/LoadRegion.svelte';
24
29
  export { default as EmptyState } from './components/EmptyState.svelte';
25
30
  export { default as ErrorState } from './components/ErrorState.svelte';
31
+ export { default as Notice } from './components/Notice.svelte';
26
32
  export { default as Popover } from './components/Popover.svelte';
27
33
  export { default as Menu } from './components/Menu.svelte';
28
34
  export { default as MenuItem } from './components/MenuItem.svelte';
package/dist/tokens.css CHANGED
@@ -179,6 +179,10 @@
179
179
  --rail-width: 80px;
180
180
  --sidebar-width: 240px;
181
181
  --drawer-width: 320px;
182
+ /* ListDetail: the list pane's width side by side, gutters included. The
183
+ narrowest phone's width, so a list laid out for the phone screen fits
184
+ the column as it is. */
185
+ --list-detail-width: 320px;
182
186
  }
183
187
 
184
188
  /* Light palette (2026-11-11). An app picks the theme with data-theme on
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mystravil/hardpoint",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Hardpoint: a Svelte 5 design system on Bits UI. AMOLED black, titanium ink, Geist, square solid blocks, banded mono tables, opt-in expressive motion, dark and light themes.",
5
5
  "license": "MIT",
6
6
  "author": "MYSTRAVIL",