@mystravil/hardpoint 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/LICENSE +25 -0
  3. package/README.md +64 -3
  4. package/dist/base.css +346 -0
  5. package/dist/components/AppShell.svelte +698 -0
  6. package/dist/components/AppShell.svelte.d.ts +77 -0
  7. package/dist/components/Button.svelte +174 -0
  8. package/dist/components/Button.svelte.d.ts +20 -0
  9. package/dist/components/Checkbox.svelte +198 -0
  10. package/dist/components/Checkbox.svelte.d.ts +16 -0
  11. package/dist/components/CheckboxGroup.svelte +99 -0
  12. package/dist/components/CheckboxGroup.svelte.d.ts +25 -0
  13. package/dist/components/Combobox.svelte +824 -0
  14. package/dist/components/Combobox.svelte.d.ts +63 -0
  15. package/dist/components/Dialog.svelte +356 -0
  16. package/dist/components/Dialog.svelte.d.ts +43 -0
  17. package/dist/components/Disclosure.svelte +201 -0
  18. package/dist/components/Disclosure.svelte.d.ts +27 -0
  19. package/dist/components/EmptyState.svelte +88 -0
  20. package/dist/components/EmptyState.svelte.d.ts +22 -0
  21. package/dist/components/ErrorState.svelte +135 -0
  22. package/dist/components/ErrorState.svelte.d.ts +26 -0
  23. package/dist/components/KeyValueList.svelte +114 -0
  24. package/dist/components/KeyValueList.svelte.d.ts +43 -0
  25. package/dist/components/LoadRegion.svelte +219 -0
  26. package/dist/components/LoadRegion.svelte.d.ts +27 -0
  27. package/dist/components/MediaFrame.svelte +110 -0
  28. package/dist/components/MediaFrame.svelte.d.ts +20 -0
  29. package/dist/components/MediaImage.svelte +256 -0
  30. package/dist/components/MediaImage.svelte.d.ts +16 -0
  31. package/dist/components/Menu.svelte +258 -0
  32. package/dist/components/Menu.svelte.d.ts +36 -0
  33. package/dist/components/MenuCheckboxItem.svelte +119 -0
  34. package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
  35. package/dist/components/MenuGroup.svelte +50 -0
  36. package/dist/components/MenuGroup.svelte.d.ts +15 -0
  37. package/dist/components/MenuItem.svelte +94 -0
  38. package/dist/components/MenuItem.svelte.d.ts +19 -0
  39. package/dist/components/MenuRadioGroup.svelte +62 -0
  40. package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
  41. package/dist/components/MenuRadioItem.svelte +110 -0
  42. package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
  43. package/dist/components/MenuSeparator.svelte +28 -0
  44. package/dist/components/MenuSeparator.svelte.d.ts +6 -0
  45. package/dist/components/NativeSelect.svelte +119 -0
  46. package/dist/components/NativeSelect.svelte.d.ts +46 -0
  47. package/dist/components/Pagination.svelte +132 -0
  48. package/dist/components/Pagination.svelte.d.ts +18 -0
  49. package/dist/components/Popover.svelte +222 -0
  50. package/dist/components/Popover.svelte.d.ts +35 -0
  51. package/dist/components/Progress.svelte +157 -0
  52. package/dist/components/Progress.svelte.d.ts +34 -0
  53. package/dist/components/Select.svelte +405 -0
  54. package/dist/components/Select.svelte.d.ts +42 -0
  55. package/dist/components/Skeleton.svelte +149 -0
  56. package/dist/components/Skeleton.svelte.d.ts +24 -0
  57. package/dist/components/Spinner.svelte +117 -0
  58. package/dist/components/Spinner.svelte.d.ts +13 -0
  59. package/dist/components/Stat.svelte +253 -0
  60. package/dist/components/Stat.svelte.d.ts +46 -0
  61. package/dist/components/Steps.svelte +189 -0
  62. package/dist/components/Steps.svelte.d.ts +31 -0
  63. package/dist/components/Switch.svelte +222 -0
  64. package/dist/components/Switch.svelte.d.ts +18 -0
  65. package/dist/components/Table.svelte +504 -0
  66. package/dist/components/Table.svelte.d.ts +92 -0
  67. package/dist/components/Tabs.svelte +246 -0
  68. package/dist/components/Tabs.svelte.d.ts +44 -0
  69. package/dist/components/Tag.svelte +40 -0
  70. package/dist/components/Tag.svelte.d.ts +15 -0
  71. package/dist/components/TextField.svelte +256 -0
  72. package/dist/components/TextField.svelte.d.ts +32 -0
  73. package/dist/components/Timeline.svelte +156 -0
  74. package/dist/components/Timeline.svelte.d.ts +30 -0
  75. package/dist/components/Toaster.svelte +411 -0
  76. package/dist/components/Toaster.svelte.d.ts +11 -0
  77. package/dist/components/Tooltip.svelte +200 -0
  78. package/dist/components/Tooltip.svelte.d.ts +38 -0
  79. package/dist/components/TooltipGroup.svelte +46 -0
  80. package/dist/components/TooltipGroup.svelte.d.ts +29 -0
  81. package/dist/components/internal/Scrim.svelte +52 -0
  82. package/dist/components/internal/Scrim.svelte.d.ts +15 -0
  83. package/dist/components/internal/Sheet.svelte +263 -0
  84. package/dist/components/internal/Sheet.svelte.d.ts +36 -0
  85. package/dist/components/internal/fade-in.d.ts +10 -0
  86. package/dist/components/internal/fade-in.js +20 -0
  87. package/dist/components/internal/resolve.d.ts +8 -0
  88. package/dist/components/internal/resolve.js +247 -0
  89. package/dist/components/internal/swipe-dismiss.d.ts +47 -0
  90. package/dist/components/internal/swipe-dismiss.js +220 -0
  91. package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
  92. package/dist/components/internal/visual-viewport.svelte.js +79 -0
  93. package/dist/components/toast.svelte.d.ts +57 -0
  94. package/dist/components/toast.svelte.js +84 -0
  95. package/dist/fonts/OFL-geist-pixel.txt +92 -0
  96. package/dist/fonts/OFL.txt +93 -0
  97. package/dist/fonts/geist-latin-variable.woff2 +0 -0
  98. package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
  99. package/dist/fonts/geist-pixel-square.woff2 +0 -0
  100. package/dist/fonts.css +71 -0
  101. package/dist/index.d.ts +39 -0
  102. package/dist/index.js +39 -0
  103. package/dist/motion/Motion.svelte +15 -0
  104. package/dist/motion/Motion.svelte.d.ts +9 -0
  105. package/dist/motion/context.d.ts +18 -0
  106. package/dist/motion/context.js +26 -0
  107. package/dist/motion/index.d.ts +3 -0
  108. package/dist/motion/index.js +3 -0
  109. package/dist/motion/transitions.d.ts +31 -0
  110. package/dist/motion/transitions.js +112 -0
  111. package/dist/tokens.css +279 -0
  112. package/package.json +76 -6
@@ -0,0 +1,46 @@
1
+ import type { HTMLSelectAttributes } from 'svelte/elements';
2
+ export type NativeSelectOption<V = string> = {
3
+ value: V;
4
+ label: string;
5
+ disabled?: boolean;
6
+ };
7
+ /** From which width the label sits beside the well: 600px, 900px or 1200px. */
8
+ export type NativeSelectBreakpoint = 'medium' | 'expanded' | 'large';
9
+ import type { Snippet } from 'svelte';
10
+ declare function $$render<V = string>(): {
11
+ props: Omit<HTMLSelectAttributes, "children" | "value" | "multiple"> & {
12
+ /** The caps field label. */
13
+ label: string;
14
+ /** The chosen option's value. Bindable. */
15
+ value?: V;
16
+ /** The choices. Without it, pass `<option>` elements as children. */
17
+ options?: NativeSelectOption<V>[];
18
+ /**
19
+ * The label sits above the well by default. `true` puts it beside the
20
+ * well at every width; a breakpoint puts it beside from that width up.
21
+ */
22
+ labelBeside?: boolean | NativeSelectBreakpoint;
23
+ children?: Snippet;
24
+ };
25
+ exports: {};
26
+ bindings: "value";
27
+ slots: {};
28
+ events: {};
29
+ };
30
+ declare class __sveltets_Render<V = string> {
31
+ props(): ReturnType<typeof $$render<V>>['props'];
32
+ events(): ReturnType<typeof $$render<V>>['events'];
33
+ slots(): ReturnType<typeof $$render<V>>['slots'];
34
+ bindings(): "value";
35
+ exports(): {};
36
+ }
37
+ interface $$IsomorphicComponent {
38
+ new <V = string>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<V>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<V>['props']>, ReturnType<__sveltets_Render<V>['events']>, ReturnType<__sveltets_Render<V>['slots']>> & {
39
+ $$bindings?: ReturnType<__sveltets_Render<V>['bindings']>;
40
+ } & ReturnType<__sveltets_Render<V>['exports']>;
41
+ <V = string>(internal: unknown, props: ReturnType<__sveltets_Render<V>['props']> & {}): ReturnType<__sveltets_Render<V>['exports']>;
42
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
43
+ }
44
+ declare const NativeSelect: $$IsomorphicComponent;
45
+ type NativeSelect<V = string> = InstanceType<typeof NativeSelect<V>>;
46
+ export default NativeSelect;
@@ -0,0 +1,132 @@
1
+ <script lang="ts" module>
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+
4
+ export type PaginationProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
5
+ /** The current page, from 1. Bindable. */
6
+ page?: number;
7
+ /** The number of items. */
8
+ count: number;
9
+ /** Items per page. With the default 1, `count` is the number of pages. */
10
+ perPage?: number;
11
+ /** The navigation landmark's accessible name. */
12
+ label?: string;
13
+ /** The caps label before the page figure. */
14
+ pageLabel?: string;
15
+ previousLabel?: string;
16
+ nextLabel?: string;
17
+ };
18
+ </script>
19
+
20
+ <script lang="ts">
21
+ import { Pagination as BitsPagination } from 'bits-ui';
22
+
23
+ let {
24
+ page = $bindable(1),
25
+ count,
26
+ perPage = 1,
27
+ label = 'Pages',
28
+ pageLabel = 'Page',
29
+ previousLabel = 'Previous page',
30
+ nextLabel = 'Next page',
31
+ class: className,
32
+ ...rest
33
+ }: PaginationProps = $props();
34
+
35
+ const pages = $derived(Math.max(1, Math.ceil(count / perPage)));
36
+
37
+ // Fewer items (a filter) can leave the page past the end; keep it in range.
38
+ $effect.pre(() => {
39
+ if (page > pages) page = pages;
40
+ });
41
+ </script>
42
+
43
+ <!--
44
+ The list view's one group of solid blocks: square titanium steps with
45
+ black arrows. Bits UI Pagination supplies the behaviour: disabled ends and
46
+ arrow keys between the steps.
47
+ -->
48
+ <BitsPagination.Root bind:page {count} {perPage}>
49
+ {#snippet child({ props, currentPage })}
50
+ <nav {...rest} {...props} class={['hp-pagination', className]} aria-label={label}>
51
+ <span class="hp-label">{pageLabel}</span>
52
+ <span class="figure" aria-live="polite">{currentPage} / {pages}</span>
53
+ <span class="steps">
54
+ <BitsPagination.PrevButton>
55
+ {#snippet child({ props })}
56
+ <button {...props} class="step" data-direction="previous" aria-label={previousLabel}
57
+ ></button>
58
+ {/snippet}
59
+ </BitsPagination.PrevButton>
60
+ <BitsPagination.NextButton>
61
+ {#snippet child({ props })}
62
+ <button {...props} class="step" data-direction="next" aria-label={nextLabel}></button>
63
+ {/snippet}
64
+ </BitsPagination.NextButton>
65
+ </span>
66
+ </nav>
67
+ {/snippet}
68
+ </BitsPagination.Root>
69
+
70
+ <style>
71
+ .hp-pagination {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: var(--space-4);
75
+ }
76
+ .figure {
77
+ font-family: var(--mono);
78
+ font-variant-numeric: tabular-nums;
79
+ }
80
+ /* 8px apart, so a focus contour (4px out) never overlaps the neighbour. */
81
+ .steps {
82
+ display: flex;
83
+ gap: var(--space-2);
84
+ margin-left: var(--space-2);
85
+ }
86
+
87
+ /* 24px on fine pointers, 44px on coarse ones (--hit-target). A
88
+ disabled step drops to the band grey. */
89
+ .step {
90
+ display: grid;
91
+ place-items: center;
92
+ width: var(--hit-target);
93
+ height: var(--hit-target);
94
+ padding: 0;
95
+ border: 0;
96
+ background: var(--accent);
97
+ color: var(--canvas);
98
+ cursor: pointer;
99
+ -webkit-tap-highlight-color: transparent;
100
+ }
101
+ .step::before {
102
+ content: '';
103
+ width: 6px;
104
+ height: 8px;
105
+ background: currentColor;
106
+ }
107
+ @media (pointer: coarse) {
108
+ .step::before {
109
+ width: 8px;
110
+ height: 12px;
111
+ }
112
+ }
113
+ .step[data-direction='previous']::before {
114
+ clip-path: polygon(100% 0, 0 50%, 100% 100%);
115
+ }
116
+ .step[data-direction='next']::before {
117
+ clip-path: polygon(0 0, 100% 50%, 0 100%);
118
+ }
119
+ .step:disabled {
120
+ background: var(--surface-band);
121
+ color: var(--muted);
122
+ cursor: default;
123
+ }
124
+ @media (hover: hover) and (pointer: fine) {
125
+ .step:not(:disabled):hover {
126
+ background: var(--muted);
127
+ }
128
+ }
129
+ .step:not(:disabled):active {
130
+ background: var(--muted);
131
+ }
132
+ </style>
@@ -0,0 +1,18 @@
1
+ import type { HTMLAttributes } from 'svelte/elements';
2
+ export type PaginationProps = Omit<HTMLAttributes<HTMLElement>, 'children'> & {
3
+ /** The current page, from 1. Bindable. */
4
+ page?: number;
5
+ /** The number of items. */
6
+ count: number;
7
+ /** Items per page. With the default 1, `count` is the number of pages. */
8
+ perPage?: number;
9
+ /** The navigation landmark's accessible name. */
10
+ label?: string;
11
+ /** The caps label before the page figure. */
12
+ pageLabel?: string;
13
+ previousLabel?: string;
14
+ nextLabel?: string;
15
+ };
16
+ declare const Pagination: import("svelte").Component<PaginationProps, {}, "page">;
17
+ type Pagination = ReturnType<typeof Pagination>;
18
+ export default Pagination;
@@ -0,0 +1,222 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import { Popover as BitsPopover } from 'bits-ui';
4
+
5
+ export type PopoverTriggerSnippetProps = {
6
+ /** Spread onto the button that opens the popover. */
7
+ props: Record<string, unknown>;
8
+ };
9
+
10
+ export type PopoverActionsSnippetProps = {
11
+ /** Closes the popover; focus returns to the trigger. */
12
+ close: () => void;
13
+ };
14
+
15
+ /**
16
+ * Attributes and handlers not listed here go to the panel (Bits UI
17
+ * `Popover.Content`), among them `side` (default `bottom`), `align`
18
+ * (default `start`), `sideOffset`, `onOpenAutoFocus`, `onCloseAutoFocus`,
19
+ * `onEscapeKeydown` and `onInteractOutside`. The placement props apply
20
+ * from 600px only; below it the panel is a bottom sheet.
21
+ */
22
+ export type PopoverProps = Omit<
23
+ BitsPopover.ContentProps,
24
+ 'child' | 'children' | 'forceMount' | 'ref' | 'title'
25
+ > & {
26
+ open?: boolean;
27
+ /** The caps title, and the panel's accessible name. */
28
+ title: string;
29
+ /** The button the panel is anchored to: spread `props` onto it. */
30
+ trigger: Snippet<[PopoverTriggerSnippetProps]>;
31
+ /** The body, under the title. */
32
+ children: Snippet;
33
+ /** The action row at the bottom, primary action last. */
34
+ actions?: Snippet<[PopoverActionsSnippetProps]>;
35
+ /** The sheet's close button's accessible name (below 600px). */
36
+ closeLabel?: string;
37
+ /** The panel element. */
38
+ ref?: HTMLElement | null;
39
+ };
40
+ </script>
41
+
42
+ <script lang="ts">
43
+ import { MediaQuery } from 'svelte/reactivity';
44
+ import { pop } from '../motion/index.js';
45
+ import Sheet from './internal/Sheet.svelte';
46
+
47
+ const uid = $props.id();
48
+
49
+ let {
50
+ open = $bindable(false),
51
+ title,
52
+ trigger,
53
+ children,
54
+ actions,
55
+ closeLabel = 'Close',
56
+ ref = $bindable(null),
57
+ class: className,
58
+ ...rest
59
+ }: PopoverProps = $props();
60
+
61
+ const titleId = `${uid}-title`;
62
+
63
+ // The form follows the width (BRIEF, Apps and phones: Overlays): a panel
64
+ // anchored to the trigger from 600px, the shared bottom sheet below it.
65
+ const wide = new MediaQuery('min-width: 600px', true);
66
+ const form = $derived(wide.current ? 'popover' : 'sheet');
67
+
68
+ function close() {
69
+ open = false;
70
+ }
71
+ </script>
72
+
73
+ <!--
74
+ Bits UI gives the trigger aria-haspopup="dialog", aria-expanded and
75
+ aria-controls; the panel takes role="dialog" and its title as its name.
76
+ Focus moves into the panel on open (to the sheet's close button on
77
+ compact) and returns to the trigger on close. Escape, a press outside,
78
+ the trigger and, on compact, the close button and a swipe down close it.
79
+ -->
80
+ <BitsPopover.Root bind:open>
81
+ <BitsPopover.Trigger>
82
+ {#snippet child({ props })}
83
+ {@render trigger({ props })}
84
+ {/snippet}
85
+ </BitsPopover.Trigger>
86
+
87
+ <BitsPopover.Portal>
88
+ {#if wide.current}
89
+ <BitsPopover.Content
90
+ forceMount
91
+ bind:ref
92
+ role="dialog"
93
+ aria-labelledby={titleId}
94
+ side="bottom"
95
+ align="start"
96
+ sideOffset={8}
97
+ collisionPadding={8}
98
+ {...rest}
99
+ >
100
+ {#snippet child({ wrapperProps, props, open: shown })}
101
+ {#if shown}
102
+ <div {...wrapperProps}>
103
+ <div {...props} class={['hp-popover', className]} data-form="popover" transition:pop>
104
+ <p id={titleId} class="hp-popover-title hp-label">{title}</p>
105
+ {@render body()}
106
+ {#if actions}
107
+ {@render actionRow()}
108
+ {/if}
109
+ </div>
110
+ </div>
111
+ {/if}
112
+ {/snippet}
113
+ </BitsPopover.Content>
114
+ {:else}
115
+ <BitsPopover.ContentStatic forceMount preventScroll bind:ref role="dialog" aria-labelledby={titleId} {...rest}>
116
+ {#snippet child({ props, open: shown })}
117
+ <Sheet
118
+ {...props}
119
+ class={['hp-popover', className]}
120
+ data-form="sheet"
121
+ open={shown}
122
+ layer="popover"
123
+ {title}
124
+ {titleId}
125
+ {closeLabel}
126
+ onclose={close}
127
+ footer={actions ? actionRow : undefined}
128
+ >
129
+ {@render body()}
130
+ </Sheet>
131
+ {/snippet}
132
+ </BitsPopover.ContentStatic>
133
+ {/if}
134
+ </BitsPopover.Portal>
135
+ </BitsPopover.Root>
136
+
137
+ {#snippet body()}
138
+ <div class="hp-popover-body" data-form={form} data-end={actions ? undefined : ''}>
139
+ {@render children()}
140
+ </div>
141
+ {/snippet}
142
+
143
+ {#snippet actionRow()}
144
+ <div class="hp-popover-actions" data-form={form}>
145
+ {@render actions?.({ close })}
146
+ </div>
147
+ {/snippet}
148
+
149
+ <style>
150
+ /* ---- Popover (600px and up): a black panel under its trigger --------- */
151
+
152
+ /* On the popover layer (tokens.css), over the shell and over an open
153
+ dialog. Bits UI copies the z-index to the positioned wrapper. As wide
154
+ as its content up to --popover-width (an app passes
155
+ style="--popover-width: …" for another cap; the sheet ignores it), and
156
+ as tall as the space beside the trigger; the body scrolls inside. */
157
+ .hp-popover {
158
+ --popover-width: 360px;
159
+
160
+ position: relative;
161
+ z-index: var(--layer-popover);
162
+ display: flex;
163
+ flex-direction: column;
164
+ box-sizing: border-box;
165
+ width: max-content;
166
+ max-width: min(var(--popover-width), var(--bits-popover-content-available-width));
167
+ max-height: var(--bits-popover-content-available-height);
168
+ border: 1px solid var(--border-control);
169
+ background: var(--canvas);
170
+ color: var(--text);
171
+ overscroll-behavior: contain;
172
+ }
173
+
174
+ /* A caps label names the panel, in the main ink, so the muted caps
175
+ labels of fields inside it read as one level down. */
176
+ .hp-popover-title {
177
+ flex: none;
178
+ margin: 0;
179
+ padding: var(--space-3) var(--space-4) 0;
180
+ }
181
+
182
+ /* ---- Body and actions, in both forms -------------------------------- */
183
+
184
+ /* The top padding keeps a first control's focus contour (4px out, 2px
185
+ wide) inside the scrolling box. */
186
+ .hp-popover-body {
187
+ display: grid;
188
+ align-content: start;
189
+ gap: var(--space-4);
190
+ min-width: 0;
191
+ font-size: var(--type-body);
192
+ line-height: 24px;
193
+ }
194
+ .hp-popover-body[data-form='popover'] {
195
+ flex: 1 1 auto;
196
+ min-height: 0;
197
+ padding: var(--space-3) var(--space-4);
198
+ overflow-y: auto;
199
+ overscroll-behavior: contain;
200
+ }
201
+ /* The sheet's body scrolls itself. */
202
+ .hp-popover-body[data-form='sheet'] {
203
+ padding: var(--space-2) var(--gutter);
204
+ }
205
+ /* Without an action row the body ends the panel. */
206
+ .hp-popover-body[data-end] {
207
+ padding-bottom: var(--space-4);
208
+ }
209
+
210
+ /* Actions sit at the end, primary last, 8px apart. */
211
+ .hp-popover-actions {
212
+ flex: none;
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ justify-content: flex-end;
216
+ gap: var(--space-2);
217
+ padding: var(--space-4) var(--gutter);
218
+ }
219
+ .hp-popover-actions[data-form='popover'] {
220
+ padding: var(--space-1) var(--space-4) var(--space-4);
221
+ }
222
+ </style>
@@ -0,0 +1,35 @@
1
+ import type { Snippet } from 'svelte';
2
+ import { Popover as BitsPopover } from 'bits-ui';
3
+ export type PopoverTriggerSnippetProps = {
4
+ /** Spread onto the button that opens the popover. */
5
+ props: Record<string, unknown>;
6
+ };
7
+ export type PopoverActionsSnippetProps = {
8
+ /** Closes the popover; focus returns to the trigger. */
9
+ close: () => void;
10
+ };
11
+ /**
12
+ * Attributes and handlers not listed here go to the panel (Bits UI
13
+ * `Popover.Content`), among them `side` (default `bottom`), `align`
14
+ * (default `start`), `sideOffset`, `onOpenAutoFocus`, `onCloseAutoFocus`,
15
+ * `onEscapeKeydown` and `onInteractOutside`. The placement props apply
16
+ * from 600px only; below it the panel is a bottom sheet.
17
+ */
18
+ export type PopoverProps = Omit<BitsPopover.ContentProps, 'child' | 'children' | 'forceMount' | 'ref' | 'title'> & {
19
+ open?: boolean;
20
+ /** The caps title, and the panel's accessible name. */
21
+ title: string;
22
+ /** The button the panel is anchored to: spread `props` onto it. */
23
+ trigger: Snippet<[PopoverTriggerSnippetProps]>;
24
+ /** The body, under the title. */
25
+ children: Snippet;
26
+ /** The action row at the bottom, primary action last. */
27
+ actions?: Snippet<[PopoverActionsSnippetProps]>;
28
+ /** The sheet's close button's accessible name (below 600px). */
29
+ closeLabel?: string;
30
+ /** The panel element. */
31
+ ref?: HTMLElement | null;
32
+ };
33
+ declare const Popover: import("svelte").Component<PopoverProps, {}, "ref" | "open">;
34
+ type Popover = ReturnType<typeof Popover>;
35
+ export default Popover;
@@ -0,0 +1,157 @@
1
+ <script lang="ts" module>
2
+ import { Progress as BitsProgress } from 'bits-ui';
3
+
4
+ export type ProgressProps = Omit<BitsProgress.RootProps, 'value' | 'child' | 'children'> & {
5
+ /**
6
+ * How far the work has got, from `min` to `max`. Pass the real figure
7
+ * each time it changes: the bar never moves on its own. Work whose
8
+ * progress is unknown shows a `Spinner` instead.
9
+ */
10
+ value: number;
11
+ /** The value at which the work is done. Default 100. */
12
+ max?: number;
13
+ /** The value at which the work starts. Default 0. */
14
+ min?: number;
15
+ /**
16
+ * How many segments the bar has (default 20, at least 2). A segment
17
+ * lights once the value has passed its whole share, so the bar never
18
+ * shows more than has been done.
19
+ */
20
+ segments?: number;
21
+ /**
22
+ * A line above the bar saying what is in progress, as written (never
23
+ * caps: it may hold a file or project name). It names the progress
24
+ * bar; without it, pass `aria-label` or `aria-labelledby`.
25
+ */
26
+ label?: string;
27
+ /**
28
+ * The value in words, such as `3 of 12 files`. Shown at the end of the
29
+ * label line and read as `aria-valuetext`. Without it both give the
30
+ * percentage done, rounded down.
31
+ */
32
+ valueText?: string;
33
+ };
34
+ </script>
35
+
36
+ <script lang="ts">
37
+ let {
38
+ value,
39
+ max = 100,
40
+ min = 0,
41
+ segments = 20,
42
+ label,
43
+ valueText,
44
+ id,
45
+ class: className,
46
+ ref = $bindable(null),
47
+ ...rest
48
+ }: ProgressProps = $props();
49
+
50
+ const uid = $props.id();
51
+ const labelId = `${uid}-label`;
52
+
53
+ // The value inside its range, so the bar and aria-valuenow agree. An
54
+ // empty range counts as done.
55
+ const current = $derived(Math.min(max, Math.max(min, value)));
56
+ const span = $derived(max - min);
57
+ const count = $derived(Math.max(2, Math.floor(segments)));
58
+ // Round down: a segment lights only when its whole share is done.
59
+ // Multiply before dividing and clear float noise before rounding down,
60
+ // so 58 of 100 and 0.58 of 1 are both 58% and not 57.99…
61
+ const share = (n: number) => Math.floor(Number((((current - min) * n) / span).toPrecision(12)));
62
+ const lit = $derived(current >= max || span <= 0 ? count : share(count));
63
+ const percent = $derived(span > 0 ? share(100) : 100);
64
+ const shown = $derived(valueText ?? `${percent}%`);
65
+ </script>
66
+
67
+ <!--
68
+ Bits UI Progress, rendered through its `child` snippet so the element is
69
+ this component's markup and its styles stay scoped. The root is the
70
+ progressbar (`role`, `aria-valuemin`, `aria-valuemax`, `aria-valuenow`,
71
+ `data-state` loading | loaded); `id`, extra attributes and `class` go to
72
+ it. Its children are presentational to assistive technology, so the label
73
+ line names it through `aria-labelledby`, and the value as shown (the
74
+ `valueText` or the floored percentage) reaches it as `aria-valuetext`.
75
+ A segment that lights or goes out fades its colour over --dur-segment.
76
+ -->
77
+ <BitsProgress.Root
78
+ bind:ref
79
+ value={current}
80
+ {max}
81
+ {min}
82
+ aria-labelledby={label ? labelId : undefined}
83
+ aria-valuetext={shown}
84
+ {...rest}
85
+ >
86
+ {#snippet child({ props })}
87
+ <div {...props} {id} class={['hp-progress', className]} data-lit={lit} data-segments={count}>
88
+ {#if label}
89
+ <div class="hp-progress-line">
90
+ <span id={labelId} class="hp-progress-label">{label}</span>
91
+ <span class="hp-progress-value">{shown}</span>
92
+ </div>
93
+ {/if}
94
+ <div class="hp-progress-bar" style:--segments={count}>
95
+ {#each { length: count }, i}
96
+ <span class="hp-progress-segment" data-lit={i < lit || undefined}></span>
97
+ {/each}
98
+ </div>
99
+ </div>
100
+ {/snippet}
101
+ </BitsProgress.Root>
102
+
103
+ <style>
104
+ /* The bar fills the width it is given; the app sets a narrower one. */
105
+ .hp-progress {
106
+ display: grid;
107
+ gap: var(--space-2);
108
+ min-width: 0;
109
+ }
110
+
111
+ /* 16px on touch, the dense 14px on fine pointers. The value keeps the
112
+ end of the line; a long label wraps rather than widen the page. */
113
+ .hp-progress-line {
114
+ display: flex;
115
+ align-items: baseline;
116
+ justify-content: space-between;
117
+ gap: var(--space-4);
118
+ font-size: var(--type-body);
119
+ line-height: 24px;
120
+ }
121
+ @media (pointer: fine) {
122
+ .hp-progress-line {
123
+ font-size: var(--type-dense);
124
+ line-height: 20px;
125
+ }
126
+ }
127
+ .hp-progress-label {
128
+ min-width: 0;
129
+ overflow-wrap: anywhere;
130
+ }
131
+ .hp-progress-value {
132
+ flex: none;
133
+ color: var(--muted);
134
+ font-variant-numeric: tabular-nums;
135
+ }
136
+
137
+ /* Separate segments (BRIEF, visual rule 7; da3-38): titanium once done,
138
+ the 3:1 control boundary while still to do, so the unlit share reads as
139
+ part of the bar. Square, 4px apart, one row. */
140
+ .hp-progress-bar {
141
+ display: grid;
142
+ grid-template-columns: repeat(var(--segments), minmax(0, 1fr));
143
+ gap: var(--space-1);
144
+ height: 12px;
145
+ }
146
+ /* A segment that lights or goes out fades its colour over --dur-segment
147
+ (BRIEF, short motion). A CSS transition never runs on an element's
148
+ first style, so a bar shows its value at once when it renders. 0ms is
149
+ a snap. Colour only, so reduced motion keeps it. */
150
+ .hp-progress-segment {
151
+ background: var(--border-control);
152
+ transition: background-color var(--dur-segment) var(--settle);
153
+ }
154
+ .hp-progress-segment[data-lit] {
155
+ background: var(--accent);
156
+ }
157
+ </style>
@@ -0,0 +1,34 @@
1
+ import { Progress as BitsProgress } from 'bits-ui';
2
+ export type ProgressProps = Omit<BitsProgress.RootProps, 'value' | 'child' | 'children'> & {
3
+ /**
4
+ * How far the work has got, from `min` to `max`. Pass the real figure
5
+ * each time it changes: the bar never moves on its own. Work whose
6
+ * progress is unknown shows a `Spinner` instead.
7
+ */
8
+ value: number;
9
+ /** The value at which the work is done. Default 100. */
10
+ max?: number;
11
+ /** The value at which the work starts. Default 0. */
12
+ min?: number;
13
+ /**
14
+ * How many segments the bar has (default 20, at least 2). A segment
15
+ * lights once the value has passed its whole share, so the bar never
16
+ * shows more than has been done.
17
+ */
18
+ segments?: number;
19
+ /**
20
+ * A line above the bar saying what is in progress, as written (never
21
+ * caps: it may hold a file or project name). It names the progress
22
+ * bar; without it, pass `aria-label` or `aria-labelledby`.
23
+ */
24
+ label?: string;
25
+ /**
26
+ * The value in words, such as `3 of 12 files`. Shown at the end of the
27
+ * label line and read as `aria-valuetext`. Without it both give the
28
+ * percentage done, rounded down.
29
+ */
30
+ valueText?: string;
31
+ };
32
+ declare const Progress: import("svelte").Component<ProgressProps, {}, "ref">;
33
+ type Progress = ReturnType<typeof Progress>;
34
+ export default Progress;