@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
@@ -1,119 +1,135 @@
1
- <script lang="ts" module>
2
- import type { HTMLSelectAttributes } from 'svelte/elements';
3
-
4
- export type NativeSelectOption<V = string> = {
5
- value: V;
6
- label: string;
7
- disabled?: boolean;
8
- };
9
-
10
- /** From which width the label sits beside the well: 600px, 900px or 1200px. */
11
- export type NativeSelectBreakpoint = 'medium' | 'expanded' | 'large';
12
- </script>
13
-
14
- <script lang="ts" generics="V = string">
15
- import type { Snippet } from 'svelte';
16
-
17
- const uid = $props.id();
18
-
19
- let {
20
- label,
21
- value = $bindable(),
22
- options,
23
- labelBeside = false,
24
- id = uid,
25
- class: className,
26
- children,
27
- ...rest
28
- }: Omit<HTMLSelectAttributes, 'value' | 'multiple' | 'children'> & {
29
- /** The caps field label. */
30
- label: string;
31
- /** The chosen option's value. Bindable. */
32
- value?: V;
33
- /** The choices. Without it, pass `<option>` elements as children. */
34
- options?: NativeSelectOption<V>[];
35
- /**
36
- * The label sits above the well by default. `true` puts it beside the
37
- * well at every width; a breakpoint puts it beside from that width up.
38
- */
39
- labelBeside?: boolean | NativeSelectBreakpoint;
40
- children?: Snippet;
41
- } = $props();
42
- </script>
43
-
44
- <!--
45
- A single choice from a short fixed list. A native <select>, so phones keep
46
- their own picker (BRIEF, Apps and phones). Extra attributes, handlers and
47
- `class` go to the <select>.
48
- -->
49
- <div
50
- class="hp-native-select"
51
- data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
52
- >
53
- <label class="hp-label" for={id}>{label}</label>
54
- <span class="hp-well-chevron">
55
- <select {...rest} {id} class={['hp-well', className]} bind:value>
56
- {#if options}
57
- {#each options as option (option.value)}
58
- <option value={option.value} disabled={option.disabled}>{option.label}</option>
59
- {/each}
60
- {:else}
61
- {@render children?.()}
62
- {/if}
63
- </select>
64
- </span>
65
- </div>
66
-
67
- <style>
68
- .hp-native-select {
69
- display: grid;
70
- gap: var(--space-1);
71
- min-width: 0;
72
- }
73
- label {
74
- color: var(--muted);
75
- }
76
- /* The well fills its column, never narrower than a short choice and the
77
- chevron. The wrapper is the grid or flex item, so it holds the minimum;
78
- a select's own minimum would be its longest option. */
79
- .hp-well-chevron {
80
- width: 100%;
81
- min-width: 136px;
82
- }
83
- select {
84
- display: block;
85
- width: 100%;
86
- }
87
-
88
- /* Beside the well, the label keeps its one line and the well gives way. */
89
- [data-label-beside] label {
90
- flex: none;
91
- }
92
-
93
- [data-label-beside='always'] {
94
- display: flex;
95
- align-items: center;
96
- gap: var(--space-2);
97
- }
98
- @media (min-width: 600px) {
99
- [data-label-beside='medium'] {
100
- display: flex;
101
- align-items: center;
102
- gap: var(--space-2);
103
- }
104
- }
105
- @media (min-width: 900px) {
106
- [data-label-beside='expanded'] {
107
- display: flex;
108
- align-items: center;
109
- gap: var(--space-2);
110
- }
111
- }
112
- @media (min-width: 1200px) {
113
- [data-label-beside='large'] {
114
- display: flex;
115
- align-items: center;
116
- gap: var(--space-2);
117
- }
118
- }
119
- </style>
1
+ <script lang="ts" module>
2
+ import type { HTMLSelectAttributes } from 'svelte/elements';
3
+
4
+ export type NativeSelectOption<V = string> = {
5
+ value: V;
6
+ label: string;
7
+ /**
8
+ * The `<optgroup>` the option sits in. Options with the same group share
9
+ * one, in the order each group first appears; options without one come
10
+ * first.
11
+ */
12
+ group?: string;
13
+ disabled?: boolean;
14
+ };
15
+
16
+ /** From which width the label sits beside the well: 600px, 900px or 1200px. */
17
+ export type NativeSelectBreakpoint = 'medium' | 'expanded' | 'large';
18
+ </script>
19
+
20
+ <script lang="ts" generics="V = string">
21
+ import type { Snippet } from 'svelte';
22
+ import { groupOptions } from './internal/groups.js';
23
+
24
+ const uid = $props.id();
25
+
26
+ let {
27
+ label,
28
+ value = $bindable(),
29
+ options,
30
+ labelBeside = false,
31
+ id = uid,
32
+ class: className,
33
+ children,
34
+ ...rest
35
+ }: Omit<HTMLSelectAttributes, 'value' | 'multiple' | 'children'> & {
36
+ /** The caps field label. */
37
+ label: string;
38
+ /** The chosen option's value. Bindable. */
39
+ value?: V;
40
+ /** The choices. Without it, pass `<option>` elements as children. */
41
+ options?: NativeSelectOption<V>[];
42
+ /**
43
+ * The label sits above the well by default. `true` puts it beside the
44
+ * well at every width; a breakpoint puts it beside from that width up.
45
+ */
46
+ labelBeside?: boolean | NativeSelectBreakpoint;
47
+ children?: Snippet;
48
+ } = $props();
49
+
50
+ const grouped = $derived(options && groupOptions(options));
51
+ </script>
52
+
53
+ <!--
54
+ A single choice from a short fixed list. A native <select>, so phones keep
55
+ their own picker (BRIEF, Apps and phones). Extra attributes, handlers and
56
+ `class` go to the <select>.
57
+ -->
58
+ <div
59
+ class="hp-native-select"
60
+ data-label-beside={labelBeside === true ? 'always' : labelBeside || undefined}
61
+ >
62
+ <label class="hp-label" for={id}>{label}</label>
63
+ <span class="hp-well-chevron">
64
+ <select {...rest} {id} class={['hp-well', className]} bind:value>
65
+ {#if grouped}
66
+ {#each grouped.loose as option (option.value)}
67
+ <option value={option.value} disabled={option.disabled}>{option.label}</option>
68
+ {/each}
69
+ {#each grouped.groups as group (group.name)}
70
+ <optgroup label={group.name}>
71
+ {#each group.items as option (option.value)}
72
+ <option value={option.value} disabled={option.disabled}>{option.label}</option>
73
+ {/each}
74
+ </optgroup>
75
+ {/each}
76
+ {:else}
77
+ {@render children?.()}
78
+ {/if}
79
+ </select>
80
+ </span>
81
+ </div>
82
+
83
+ <style>
84
+ .hp-native-select {
85
+ display: grid;
86
+ gap: var(--space-1);
87
+ min-width: 0;
88
+ }
89
+ label {
90
+ color: var(--muted);
91
+ }
92
+ /* The well fills its column, never narrower than a short choice and the
93
+ chevron. The wrapper is the grid or flex item, so it holds the minimum;
94
+ a select's own minimum would be its longest option. */
95
+ .hp-well-chevron {
96
+ width: 100%;
97
+ min-width: 136px;
98
+ }
99
+ select {
100
+ display: block;
101
+ width: 100%;
102
+ }
103
+
104
+ /* Beside the well, the label keeps its one line and the well gives way. */
105
+ [data-label-beside] label {
106
+ flex: none;
107
+ }
108
+
109
+ [data-label-beside='always'] {
110
+ display: flex;
111
+ align-items: center;
112
+ gap: var(--space-2);
113
+ }
114
+ @media (min-width: 600px) {
115
+ [data-label-beside='medium'] {
116
+ display: flex;
117
+ align-items: center;
118
+ gap: var(--space-2);
119
+ }
120
+ }
121
+ @media (min-width: 900px) {
122
+ [data-label-beside='expanded'] {
123
+ display: flex;
124
+ align-items: center;
125
+ gap: var(--space-2);
126
+ }
127
+ }
128
+ @media (min-width: 1200px) {
129
+ [data-label-beside='large'] {
130
+ display: flex;
131
+ align-items: center;
132
+ gap: var(--space-2);
133
+ }
134
+ }
135
+ </style>
@@ -2,6 +2,12 @@ import type { HTMLSelectAttributes } from 'svelte/elements';
2
2
  export type NativeSelectOption<V = string> = {
3
3
  value: V;
4
4
  label: string;
5
+ /**
6
+ * The `<optgroup>` the option sits in. Options with the same group share
7
+ * one, in the order each group first appears; options without one come
8
+ * first.
9
+ */
10
+ group?: string;
5
11
  disabled?: boolean;
6
12
  };
7
13
  /** From which width the label sits beside the well: 600px, 900px or 1200px. */
@@ -0,0 +1,262 @@
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import type { HTMLAttributes } from 'svelte/elements';
4
+
5
+ /** `ok`, `warn` or `danger`; absent means neutral information. The tone sets the mark's glyph and colour, never the text's. */
6
+ export type NoticeTone = 'ok' | 'warn' | 'danger';
7
+
8
+ /** The title's element: `p` by default, a heading where the notice names a region of the page. */
9
+ export type NoticeTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
10
+
11
+ export type NoticeProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
12
+ tone?: NoticeTone;
13
+ /** A short line, as written, never transformed. A notice has a title, text, or both. */
14
+ title?: string;
15
+ /** The title's element. Defaults to `p`. */
16
+ titleAs?: NoticeTitleElement;
17
+ /** The text, in the muted ink. It may hold links and inline code. */
18
+ children?: Snippet;
19
+ /**
20
+ * The app's `Button`s: at most two, the primary one last, and at most one
21
+ * primary. A notice in a view that already shows its solid group uses
22
+ * secondary or tertiary buttons.
23
+ */
24
+ actions?: Snippet;
25
+ /** Adds a close button that calls this. The app removes the notice. */
26
+ onclose?: () => void;
27
+ /** The close button's accessible name. Defaults to `Dismiss`. */
28
+ closeLabel?: string;
29
+ /** The root `div`. */
30
+ ref?: HTMLDivElement | null;
31
+ };
32
+ </script>
33
+
34
+ <script lang="ts">
35
+ import Button from './Button.svelte';
36
+
37
+ let {
38
+ tone,
39
+ title,
40
+ titleAs = 'p',
41
+ children,
42
+ actions,
43
+ onclose,
44
+ closeLabel = 'Dismiss',
45
+ ref = $bindable(null),
46
+ class: className,
47
+ ...rest
48
+ }: NoticeProps = $props();
49
+ </script>
50
+
51
+ <!--
52
+ A persistent message in the page flow: something the user should know or
53
+ act on, which stays until the app removes it (the app renders it while
54
+ it applies and gives it `onclose` when the user may dismiss it). A
55
+ confirmation is a Toast and a failed view is an ErrorState.
56
+
57
+ The Toaster's box in the flow: black, the control boundary, square, as
58
+ wide as its container. A 16px outlined square before the first line
59
+ carries the tone: "i" (neutral, muted), a check (ok), "!" (warn, danger).
60
+ It is decorative, so the words carry the status. The title is 16px
61
+ weight 500 in the text ink, never caps; the text below it is muted, 16px
62
+ on touch and 14px with a mouse. The actions sit 12px under the text. A
63
+ notice with a title and actions but no text puts the actions at the end
64
+ of the title row, and wraps them under it when they do not fit. The close
65
+ button is at the top end.
66
+
67
+ It is not a live region. Pass `role="status"` for a notice that appears
68
+ while the user is on the page, `role="alert"` for an urgent one and
69
+ `role="note"` for supplementary content present from the start. It has
70
+ no motion: it appears and goes at once. Extra attributes and `class` go
71
+ to the root `div.hp-notice`.
72
+ -->
73
+ <div
74
+ bind:this={ref}
75
+ {...rest}
76
+ class={['hp-notice', className]}
77
+ data-tone={tone}
78
+ data-titled={title ? '' : undefined}
79
+ data-row={title && !children && actions ? '' : undefined}
80
+ >
81
+ <span class="hp-notice-mark" aria-hidden="true">
82
+ <svg viewBox="0 0 16 16" width="16" height="16" fill="none" stroke="currentColor">
83
+ <path d="M1.5 1.5h13v13h-13z" />
84
+ {#if tone === 'ok'}
85
+ <path d="M4.5 8.2l2.7 2.7 4.3-5" stroke-width="2" />
86
+ {:else if tone}
87
+ <path d="M8 4v4M8 10v2" stroke-width="2" />
88
+ {:else}
89
+ <path d="M8 4v2M8 8v4" stroke-width="2" />
90
+ {/if}
91
+ </svg>
92
+ </span>
93
+ <div class="hp-notice-main">
94
+ {#if title || children}
95
+ <div class="hp-notice-content">
96
+ {#if title}
97
+ <svelte:element this={titleAs} class="hp-notice-title">{title}</svelte:element>
98
+ {/if}
99
+ {#if children}
100
+ <div class="hp-notice-body">{@render children()}</div>
101
+ {/if}
102
+ </div>
103
+ {/if}
104
+ {#if actions}
105
+ <div class="hp-notice-actions" data-after={children ? '' : undefined}>{@render actions()}</div>
106
+ {/if}
107
+ </div>
108
+ {#if onclose}
109
+ <Button
110
+ type="button"
111
+ variant="tertiary"
112
+ icon
113
+ class="hp-notice-close"
114
+ aria-label={closeLabel}
115
+ onclick={() => onclose()}
116
+ >
117
+ <svg
118
+ viewBox="0 0 16 16"
119
+ fill="none"
120
+ stroke="currentColor"
121
+ stroke-width="1.5"
122
+ stroke-linecap="square"
123
+ aria-hidden="true"
124
+ >
125
+ <path d="M3 3l10 10M13 3L3 13" />
126
+ </svg>
127
+ </Button>
128
+ {/if}
129
+ </div>
130
+
131
+ <style>
132
+ /* The Toaster's box in the flow: black, the control boundary, square.
133
+ --notice-line is the first line's height (the title's 24px, or the
134
+ text's line when there is no title); --notice-lift is how far a
135
+ control is taller than that line, halved. */
136
+ .hp-notice {
137
+ --notice-line: 24px;
138
+ --notice-text-line: 24px;
139
+ --notice-lift: calc((var(--control-height) - var(--notice-line)) / 2);
140
+
141
+ display: flex;
142
+ align-items: flex-start;
143
+ gap: var(--space-2);
144
+ box-sizing: border-box;
145
+ min-width: 0;
146
+ padding: var(--space-3);
147
+ border: 1px solid var(--border-control);
148
+ background: var(--canvas);
149
+ color: var(--text);
150
+ font-family: var(--sans);
151
+ font-size: var(--type-body);
152
+ line-height: var(--notice-text-line);
153
+ }
154
+
155
+ /* 16px where a coarse pointer reads it, the dense 14px on fine pointers. */
156
+ @media (pointer: fine) {
157
+ .hp-notice {
158
+ --notice-text-line: 20px;
159
+
160
+ font-size: var(--type-dense);
161
+ }
162
+ }
163
+
164
+ .hp-notice:not([data-titled]) {
165
+ --notice-line: var(--notice-text-line);
166
+ }
167
+
168
+ /* ---- The mark: an outlined square on the first line --------------- */
169
+
170
+ .hp-notice-mark {
171
+ display: flex;
172
+ flex: none;
173
+ align-items: center;
174
+ height: var(--notice-line);
175
+ color: var(--muted);
176
+ }
177
+ .hp-notice-mark svg {
178
+ display: block;
179
+ }
180
+ .hp-notice[data-tone='ok'] .hp-notice-mark {
181
+ color: var(--ok);
182
+ }
183
+ .hp-notice[data-tone='warn'] .hp-notice-mark {
184
+ color: var(--warn);
185
+ }
186
+ .hp-notice[data-tone='danger'] .hp-notice-mark {
187
+ color: var(--danger);
188
+ }
189
+
190
+ /* ---- Title, text and actions -------------------------------------- */
191
+
192
+ /* Wraps the actions under the content when they do not fit beside it. */
193
+ .hp-notice-main {
194
+ display: flex;
195
+ flex: 1 1 auto;
196
+ flex-wrap: wrap;
197
+ align-items: flex-start;
198
+ gap: var(--space-3) var(--space-4);
199
+ min-width: 0;
200
+ }
201
+
202
+ .hp-notice-content {
203
+ display: grid;
204
+ flex: 1 1 auto;
205
+ gap: var(--space-1);
206
+ min-width: 0;
207
+ }
208
+
209
+ .hp-notice-title {
210
+ max-width: 100%;
211
+ color: var(--text);
212
+ font-size: var(--type-body);
213
+ font-weight: 500;
214
+ line-height: 24px;
215
+ overflow-wrap: anywhere;
216
+ }
217
+
218
+ .hp-notice-body {
219
+ max-width: var(--measure);
220
+ color: var(--muted);
221
+ overflow-wrap: anywhere;
222
+ }
223
+ .hp-notice-body :global(a) {
224
+ color: var(--text);
225
+ }
226
+ .hp-notice-body :global(p + p) {
227
+ margin-top: var(--space-2);
228
+ }
229
+
230
+ .hp-notice-actions {
231
+ display: flex;
232
+ flex: none;
233
+ flex-wrap: wrap;
234
+ gap: var(--space-2);
235
+ max-width: 100%;
236
+ }
237
+ /* Text above them: they take their own row. */
238
+ .hp-notice-actions[data-after] {
239
+ flex-basis: 100%;
240
+ }
241
+
242
+ /* ---- The close button: the Toaster's, at the top end -------------- */
243
+
244
+ /* Centred on the first line; the tertiary button has no edge, so it
245
+ reaches into the padding. */
246
+ .hp-notice :global(.hp-notice-close) {
247
+ flex: none;
248
+ margin: calc(-1 * var(--notice-lift)) calc(-1 * var(--space-2)) 0 0;
249
+ }
250
+
251
+ /* A title row with actions is a control high, so the mark, the title and
252
+ the close button centre on it. */
253
+ .hp-notice[data-row] .hp-notice-mark {
254
+ margin-top: var(--notice-lift);
255
+ }
256
+ .hp-notice[data-row] .hp-notice-content {
257
+ padding-top: var(--notice-lift);
258
+ }
259
+ .hp-notice[data-row] :global(.hp-notice-close) {
260
+ margin-top: 0;
261
+ }
262
+ </style>
@@ -0,0 +1,30 @@
1
+ import type { Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ /** `ok`, `warn` or `danger`; absent means neutral information. The tone sets the mark's glyph and colour, never the text's. */
4
+ export type NoticeTone = 'ok' | 'warn' | 'danger';
5
+ /** The title's element: `p` by default, a heading where the notice names a region of the page. */
6
+ export type NoticeTitleElement = 'p' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
7
+ export type NoticeProps = Omit<HTMLAttributes<HTMLDivElement>, 'children' | 'title'> & {
8
+ tone?: NoticeTone;
9
+ /** A short line, as written, never transformed. A notice has a title, text, or both. */
10
+ title?: string;
11
+ /** The title's element. Defaults to `p`. */
12
+ titleAs?: NoticeTitleElement;
13
+ /** The text, in the muted ink. It may hold links and inline code. */
14
+ children?: Snippet;
15
+ /**
16
+ * The app's `Button`s: at most two, the primary one last, and at most one
17
+ * primary. A notice in a view that already shows its solid group uses
18
+ * secondary or tertiary buttons.
19
+ */
20
+ actions?: Snippet;
21
+ /** Adds a close button that calls this. The app removes the notice. */
22
+ onclose?: () => void;
23
+ /** The close button's accessible name. Defaults to `Dismiss`. */
24
+ closeLabel?: string;
25
+ /** The root `div`. */
26
+ ref?: HTMLDivElement | null;
27
+ };
28
+ declare const Notice: import("svelte").Component<NoticeProps, {}, "ref">;
29
+ type Notice = ReturnType<typeof Notice>;
30
+ export default Notice;
@@ -151,19 +151,19 @@
151
151
 
152
152
  /* On the popover layer (tokens.css), over the shell and over an open
153
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. */
154
+ as its content up to --popover-width, 360px unless set (an app sets it
155
+ with a class on the Popover and a rule in its stylesheet; the default
156
+ is the var() fallback, so any rule wins whatever its specificity; the
157
+ sheet ignores it), and as tall as the space beside the trigger; the
158
+ body scrolls inside. */
157
159
  .hp-popover {
158
- --popover-width: 360px;
159
-
160
160
  position: relative;
161
161
  z-index: var(--layer-popover);
162
162
  display: flex;
163
163
  flex-direction: column;
164
164
  box-sizing: border-box;
165
165
  width: max-content;
166
- max-width: min(var(--popover-width), var(--bits-popover-content-available-width));
166
+ max-width: min(var(--popover-width, 360px), var(--bits-popover-content-available-width));
167
167
  max-height: var(--bits-popover-content-available-height);
168
168
  border: 1px solid var(--border-control);
169
169
  background: var(--canvas);