@mystravil/hardpoint 0.2.0 → 0.2.1

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 (52) hide show
  1. package/CHANGELOG.md +42 -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 +174 -174
  7. package/dist/components/Checkbox.svelte +235 -235
  8. package/dist/components/CheckboxGroup.svelte +207 -207
  9. package/dist/components/Combobox.svelte +901 -901
  10. package/dist/components/Dialog.svelte +359 -359
  11. package/dist/components/Disclosure.svelte +201 -201
  12. package/dist/components/EmptyState.svelte +88 -88
  13. package/dist/components/ErrorState.svelte +135 -135
  14. package/dist/components/ImageViewer.svelte +764 -764
  15. package/dist/components/KeyValueList.svelte +114 -114
  16. package/dist/components/ListDetail.svelte +218 -218
  17. package/dist/components/LoadRegion.svelte +219 -219
  18. package/dist/components/MediaFrame.svelte +110 -110
  19. package/dist/components/MediaImage.svelte +256 -256
  20. package/dist/components/Menu.svelte +258 -258
  21. package/dist/components/MenuCheckboxItem.svelte +119 -119
  22. package/dist/components/MenuGroup.svelte +50 -50
  23. package/dist/components/MenuItem.svelte +94 -94
  24. package/dist/components/MenuRadioGroup.svelte +62 -62
  25. package/dist/components/MenuRadioItem.svelte +110 -110
  26. package/dist/components/MenuSeparator.svelte +28 -28
  27. package/dist/components/Meter.svelte +283 -283
  28. package/dist/components/NativeSelect.svelte +135 -135
  29. package/dist/components/Notice.svelte +262 -262
  30. package/dist/components/Popover.svelte +222 -222
  31. package/dist/components/Progress.svelte +157 -157
  32. package/dist/components/RadioGroup.svelte +440 -440
  33. package/dist/components/Select.svelte +471 -471
  34. package/dist/components/Skeleton.svelte +149 -149
  35. package/dist/components/Spinner.svelte +118 -118
  36. package/dist/components/Stat.svelte +253 -253
  37. package/dist/components/Steps.svelte +189 -189
  38. package/dist/components/Switch.svelte +222 -222
  39. package/dist/components/Table.svelte +504 -504
  40. package/dist/components/Tabs.svelte +246 -246
  41. package/dist/components/Tag.svelte +40 -40
  42. package/dist/components/TextArea.svelte +272 -272
  43. package/dist/components/TextField.svelte +256 -256
  44. package/dist/components/Timeline.svelte +156 -156
  45. package/dist/components/Toaster.svelte +411 -411
  46. package/dist/components/Tooltip.svelte +200 -200
  47. package/dist/components/TooltipGroup.svelte +46 -46
  48. package/dist/components/internal/Scrim.svelte +52 -52
  49. package/dist/components/internal/Sheet.svelte +263 -263
  50. package/dist/fonts.css +71 -71
  51. package/dist/tokens.css +283 -283
  52. package/package.json +81 -76
@@ -1,359 +1,359 @@
1
- <script lang="ts" module>
2
- import type { Snippet } from 'svelte';
3
- import { Dialog as BitsDialog } from 'bits-ui';
4
-
5
- export type DialogTriggerSnippetProps = {
6
- /** Spread onto the button that opens the dialog. */
7
- props: Record<string, unknown>;
8
- };
9
-
10
- export type DialogActionsSnippetProps = {
11
- /** Closes the dialog; focus returns to what had it before. */
12
- close: () => void;
13
- };
14
-
15
- /**
16
- * Attributes and handlers not listed here go to the dialog element (Bits
17
- * UI `Dialog.Content`), among them `onOpenAutoFocus`, `onCloseAutoFocus`,
18
- * `onEscapeKeydown` and `onInteractOutside`.
19
- */
20
- export type DialogProps = Omit<
21
- BitsDialog.ContentProps,
22
- 'child' | 'children' | 'forceMount' | 'ref' | 'title'
23
- > & {
24
- open?: boolean;
25
- /** The caps title and the dialog's accessible name. */
26
- title: string;
27
- /** A line in `--muted` under the title, and the accessible description. */
28
- description?: string;
29
- /**
30
- * Below 600px, a full-screen dialog instead of a bottom sheet: for long
31
- * or complex forms. While the on-screen keyboard is up, it fills the
32
- * space above it. From 600px the dialog is centred either way. In
33
- * every form a press outside does not close it, so typed input is not
34
- * lost by accident; Escape and the close button still do.
35
- */
36
- fullscreen?: boolean;
37
- /** The button that opens the dialog: spread `props` onto it. */
38
- trigger?: Snippet<[DialogTriggerSnippetProps]>;
39
- /** The body, under the title and description. */
40
- children?: Snippet;
41
- /** The action row at the bottom, primary action last. */
42
- actions?: Snippet<[DialogActionsSnippetProps]>;
43
- /** The close button's accessible name. */
44
- closeLabel?: string;
45
- /** The dialog element. */
46
- ref?: HTMLElement | null;
47
- };
48
- </script>
49
-
50
- <script lang="ts">
51
- import { MediaQuery } from 'svelte/reactivity';
52
- import { fade, sheet } from '../motion/index.js';
53
- import Scrim from './internal/Scrim.svelte';
54
- import Sheet from './internal/Sheet.svelte';
55
- import { followVisualViewport } from './internal/visual-viewport.svelte.js';
56
-
57
- const uid = $props.id();
58
-
59
- let {
60
- open = $bindable(false),
61
- title,
62
- description,
63
- fullscreen = false,
64
- trigger,
65
- children,
66
- actions,
67
- closeLabel = 'Close',
68
- ref = $bindable(null),
69
- class: className,
70
- ...rest
71
- }: DialogProps = $props();
72
-
73
- const titleId = `${uid}-title`;
74
- const descriptionId = `${uid}-description`;
75
-
76
- // The form follows the width (BRIEF, Apps and phones: Overlays): a bottom
77
- // sheet below 600px, or a full-screen dialog when asked; centred from
78
- // 600px.
79
- const wide = new MediaQuery('min-width: 600px', true);
80
- const form = $derived(wide.current ? 'centred' : fullscreen ? 'fullscreen' : 'sheet');
81
-
82
- let bodyNode = $state<HTMLElement | null>(null);
83
-
84
- // The full-screen form follows the visual viewport while it is open, as
85
- // the Sheet does (decided 2026-11-10; internal/visual-viewport.svelte.ts):
86
- // while the on-screen keyboard shortens it, the panel's top sits on its
87
- // top and the panel takes its height, so the title row, the body and the
88
- // action row stay above the keyboard, and the focused field in the body
89
- // stays in sight. A form that changes while open drops the keyboard line.
90
- const viewport = followVisualViewport(
91
- () => open && form === 'fullscreen',
92
- () => (form === 'fullscreen' ? bodyNode : null)
93
- );
94
- const lifted = $derived(form === 'fullscreen' ? viewport.current : null);
95
-
96
- // A fullscreen form ignores presses outside, so typed input is not lost;
97
- // an app can override it through rest props. Bits UI also hands the
98
- // option on in the content snippet's props; the snippet drops it there,
99
- // because it is not an HTML attribute.
100
-
101
- function close() {
102
- open = false;
103
- }
104
-
105
- // The centred dialog fades in place; the full-screen one slides up like
106
- // a sheet. Both at the dialog timing; reduced motion fades both.
107
- function panel(node: Element) {
108
- return form === 'fullscreen' ? sheet(node) : fade(node);
109
- }
110
- </script>
111
-
112
- <BitsDialog.Root bind:open>
113
- {#if trigger}
114
- <BitsDialog.Trigger>
115
- {#snippet child({ props })}
116
- {@render trigger({ props })}
117
- {/snippet}
118
- </BitsDialog.Trigger>
119
- {/if}
120
-
121
- <BitsDialog.Portal>
122
- <BitsDialog.Content
123
- forceMount
124
- bind:ref
125
- aria-labelledby={titleId}
126
- aria-describedby={description ? descriptionId : undefined}
127
- interactOutsideBehavior={fullscreen ? 'ignore' : 'close'}
128
- {...rest}
129
- >
130
- {#snippet child({ props: { interactOutsideBehavior: _, ...props }, open: shown })}
131
- {#if form === 'sheet'}
132
- <Sheet
133
- {...props}
134
- class={['hp-dialog', className]}
135
- data-form="sheet"
136
- open={shown}
137
- layer="dialog"
138
- {title}
139
- {titleId}
140
- titleTag="h2"
141
- {closeLabel}
142
- onclose={close}
143
- footer={actions ? actionRow : undefined}
144
- >
145
- {@render body()}
146
- </Sheet>
147
- {:else}
148
- <Scrim open={shown} layer="dialog" />
149
- {#if shown}
150
- <div
151
- {...props}
152
- class={['hp-dialog', className]}
153
- data-form={form}
154
- style:top={lifted ? `${lifted.top}px` : undefined}
155
- style:bottom={lifted ? 'auto' : undefined}
156
- style:height={lifted ? `${lifted.height}px` : undefined}
157
- style:padding-bottom={lifted ? '0px' : undefined}
158
- transition:panel
159
- >
160
- <div class="hp-dialog-head" data-form={form}>
161
- <h2 id={titleId} class="hp-dialog-title hp-view-title">{title}</h2>
162
- <button type="button" class="hp-dialog-close" aria-label={closeLabel} onclick={close}>
163
- <svg
164
- viewBox="0 0 16 16"
165
- fill="none"
166
- stroke="currentColor"
167
- stroke-width="1.5"
168
- stroke-linecap="square"
169
- aria-hidden="true"
170
- >
171
- <path d="M3 3l10 10M13 3L3 13" />
172
- </svg>
173
- </button>
174
- </div>
175
- {@render body()}
176
- {#if actions}
177
- {@render actionRow()}
178
- {/if}
179
- </div>
180
- {/if}
181
- {/if}
182
- {/snippet}
183
- </BitsDialog.Content>
184
- </BitsDialog.Portal>
185
- </BitsDialog.Root>
186
-
187
- {#snippet body()}
188
- <div
189
- class="hp-dialog-body"
190
- data-form={form}
191
- data-end={actions ? undefined : ''}
192
- bind:this={bodyNode}
193
- >
194
- {#if description}
195
- <p id={descriptionId} class="hp-dialog-description">{description}</p>
196
- {/if}
197
- {@render children?.()}
198
- </div>
199
- {/snippet}
200
-
201
- {#snippet actionRow()}
202
- <div class="hp-dialog-actions" data-form={form}>
203
- {@render actions?.({ close })}
204
- </div>
205
- {/snippet}
206
-
207
- <style>
208
- /* ---- Centred (600px and up) and full screen (compact) --------------- */
209
-
210
- .hp-dialog[data-form='centred'],
211
- .hp-dialog[data-form='fullscreen'] {
212
- position: fixed;
213
- z-index: var(--layer-dialog);
214
- display: flex;
215
- flex-direction: column;
216
- box-sizing: border-box;
217
- background: var(--canvas);
218
- color: var(--text);
219
- overscroll-behavior: contain;
220
- /* Bits UI may have turned pointer events off on <body>. */
221
- pointer-events: auto;
222
- }
223
-
224
- /* A square black panel with a 3:1 boundary, centred in the safe area. */
225
- .hp-dialog[data-form='centred'] {
226
- inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
227
- env(safe-area-inset-left);
228
- width: min(560px, 100% - 2 * var(--gutter));
229
- height: fit-content;
230
- max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 2 * var(--space-8));
231
- margin: auto;
232
- border: 1px solid var(--border-control);
233
- }
234
-
235
- /* The whole screen, inside the safe areas. While the on-screen keyboard
236
- is up, the visual viewport above it, without the bottom safe area,
237
- which the keyboard covers (set inline). */
238
- .hp-dialog[data-form='fullscreen'] {
239
- inset: 0;
240
- padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
241
- env(safe-area-inset-left);
242
- }
243
-
244
- /* Title row: the caps title, then the close button at the end. A long
245
- title wraps (200 percent zoom) and the button stays on its first line:
246
- the title's padding centres that line on the button, so a single line
247
- sits where the row centred it before. The title's clip stays: it keeps
248
- the first glyph's antialiasing inside the text edge, as before. */
249
- .hp-dialog-head {
250
- flex: none;
251
- display: grid;
252
- grid-template-columns: minmax(0, 1fr) auto;
253
- align-items: start;
254
- gap: var(--space-2);
255
- }
256
- .hp-dialog-head[data-form='centred'] {
257
- padding: var(--space-4) var(--space-4) 0 var(--space-6);
258
- }
259
- /* The shell header's height, so the form reads as its own view. */
260
- .hp-dialog-head[data-form='fullscreen'] {
261
- min-height: var(--header-height);
262
- padding: 0 var(--space-1) 0 var(--gutter);
263
- }
264
- .hp-dialog-title {
265
- overflow: hidden;
266
- padding-block: calc((var(--control-height) - 1lh) / 2);
267
- overflow-wrap: anywhere;
268
- }
269
- .hp-dialog-head[data-form='fullscreen'] .hp-dialog-title {
270
- padding-block: calc((var(--header-height) - 1lh) / 2);
271
- }
272
- .hp-dialog-head[data-form='fullscreen'] .hp-dialog-close {
273
- margin-block: calc((var(--header-height) - var(--control-height)) / 2);
274
- }
275
-
276
- .hp-dialog-close {
277
- display: grid;
278
- place-items: center;
279
- /* 36px on fine pointers, 44px on coarse. */
280
- width: var(--control-height);
281
- height: var(--control-height);
282
- padding: 0;
283
- border: 0;
284
- background: transparent;
285
- color: var(--text);
286
- cursor: pointer;
287
- -webkit-tap-highlight-color: transparent;
288
- }
289
- .hp-dialog-close svg {
290
- width: var(--space-4);
291
- height: var(--space-4);
292
- }
293
- .hp-dialog-close:active {
294
- background: var(--surface-raised);
295
- }
296
- @media (hover: hover) and (pointer: fine) {
297
- .hp-dialog-close {
298
- transition: background-color var(--dur-hover) var(--settle);
299
- }
300
- .hp-dialog-close:hover {
301
- background: var(--surface-hover);
302
- }
303
- .hp-dialog-close:active {
304
- background: var(--surface-raised);
305
- transition-duration: var(--dur-press);
306
- }
307
- }
308
-
309
- /* ---- Body and actions, in every form -------------------------------- */
310
-
311
- /* The top padding keeps a first field's focus contour (4px out, 2px
312
- wide) inside the scrolling box. The scroll padding keeps a focused
313
- field's caret 8px clear of the action row when the browser reveals it,
314
- as when a TextArea grows while someone types. */
315
- .hp-dialog-body {
316
- display: grid;
317
- align-content: start;
318
- gap: var(--space-4);
319
- min-width: 0;
320
- padding: var(--space-2) var(--gutter);
321
- scroll-padding-block: var(--space-2);
322
- font-size: var(--type-body);
323
- line-height: 24px;
324
- }
325
- /* The sheet's body scrolls itself; these scroll inside the panel. */
326
- .hp-dialog-body[data-form='centred'],
327
- .hp-dialog-body[data-form='fullscreen'] {
328
- flex: 1 1 auto;
329
- min-height: 0;
330
- overflow-y: auto;
331
- overscroll-behavior: contain;
332
- }
333
- .hp-dialog-body[data-form='centred'] {
334
- padding-inline: var(--space-6);
335
- }
336
- /* Without an action row the body ends the panel. */
337
- .hp-dialog-body[data-end] {
338
- padding-bottom: var(--space-4);
339
- }
340
- .hp-dialog-body[data-form='centred'][data-end] {
341
- padding-bottom: var(--space-6);
342
- }
343
- .hp-dialog-description {
344
- color: var(--muted);
345
- }
346
-
347
- /* Actions sit at the end, primary last, 8px apart. */
348
- .hp-dialog-actions {
349
- flex: none;
350
- display: flex;
351
- flex-wrap: wrap;
352
- justify-content: flex-end;
353
- gap: var(--space-2);
354
- padding: var(--space-4) var(--gutter);
355
- }
356
- .hp-dialog-actions[data-form='centred'] {
357
- padding: var(--space-6);
358
- }
359
- </style>
1
+ <script lang="ts" module>
2
+ import type { Snippet } from 'svelte';
3
+ import { Dialog as BitsDialog } from 'bits-ui';
4
+
5
+ export type DialogTriggerSnippetProps = {
6
+ /** Spread onto the button that opens the dialog. */
7
+ props: Record<string, unknown>;
8
+ };
9
+
10
+ export type DialogActionsSnippetProps = {
11
+ /** Closes the dialog; focus returns to what had it before. */
12
+ close: () => void;
13
+ };
14
+
15
+ /**
16
+ * Attributes and handlers not listed here go to the dialog element (Bits
17
+ * UI `Dialog.Content`), among them `onOpenAutoFocus`, `onCloseAutoFocus`,
18
+ * `onEscapeKeydown` and `onInteractOutside`.
19
+ */
20
+ export type DialogProps = Omit<
21
+ BitsDialog.ContentProps,
22
+ 'child' | 'children' | 'forceMount' | 'ref' | 'title'
23
+ > & {
24
+ open?: boolean;
25
+ /** The caps title and the dialog's accessible name. */
26
+ title: string;
27
+ /** A line in `--muted` under the title, and the accessible description. */
28
+ description?: string;
29
+ /**
30
+ * Below 600px, a full-screen dialog instead of a bottom sheet: for long
31
+ * or complex forms. While the on-screen keyboard is up, it fills the
32
+ * space above it. From 600px the dialog is centred either way. In
33
+ * every form a press outside does not close it, so typed input is not
34
+ * lost by accident; Escape and the close button still do.
35
+ */
36
+ fullscreen?: boolean;
37
+ /** The button that opens the dialog: spread `props` onto it. */
38
+ trigger?: Snippet<[DialogTriggerSnippetProps]>;
39
+ /** The body, under the title and description. */
40
+ children?: Snippet;
41
+ /** The action row at the bottom, primary action last. */
42
+ actions?: Snippet<[DialogActionsSnippetProps]>;
43
+ /** The close button's accessible name. */
44
+ closeLabel?: string;
45
+ /** The dialog element. */
46
+ ref?: HTMLElement | null;
47
+ };
48
+ </script>
49
+
50
+ <script lang="ts">
51
+ import { MediaQuery } from 'svelte/reactivity';
52
+ import { fade, sheet } from '../motion/index.js';
53
+ import Scrim from './internal/Scrim.svelte';
54
+ import Sheet from './internal/Sheet.svelte';
55
+ import { followVisualViewport } from './internal/visual-viewport.svelte.js';
56
+
57
+ const uid = $props.id();
58
+
59
+ let {
60
+ open = $bindable(false),
61
+ title,
62
+ description,
63
+ fullscreen = false,
64
+ trigger,
65
+ children,
66
+ actions,
67
+ closeLabel = 'Close',
68
+ ref = $bindable(null),
69
+ class: className,
70
+ ...rest
71
+ }: DialogProps = $props();
72
+
73
+ const titleId = `${uid}-title`;
74
+ const descriptionId = `${uid}-description`;
75
+
76
+ // The form follows the width (BRIEF, Apps and phones: Overlays): a bottom
77
+ // sheet below 600px, or a full-screen dialog when asked; centred from
78
+ // 600px.
79
+ const wide = new MediaQuery('min-width: 600px', true);
80
+ const form = $derived(wide.current ? 'centred' : fullscreen ? 'fullscreen' : 'sheet');
81
+
82
+ let bodyNode = $state<HTMLElement | null>(null);
83
+
84
+ // The full-screen form follows the visual viewport while it is open, as
85
+ // the Sheet does (decided 2026-11-10; internal/visual-viewport.svelte.ts):
86
+ // while the on-screen keyboard shortens it, the panel's top sits on its
87
+ // top and the panel takes its height, so the title row, the body and the
88
+ // action row stay above the keyboard, and the focused field in the body
89
+ // stays in sight. A form that changes while open drops the keyboard line.
90
+ const viewport = followVisualViewport(
91
+ () => open && form === 'fullscreen',
92
+ () => (form === 'fullscreen' ? bodyNode : null)
93
+ );
94
+ const lifted = $derived(form === 'fullscreen' ? viewport.current : null);
95
+
96
+ // A fullscreen form ignores presses outside, so typed input is not lost;
97
+ // an app can override it through rest props. Bits UI also hands the
98
+ // option on in the content snippet's props; the snippet drops it there,
99
+ // because it is not an HTML attribute.
100
+
101
+ function close() {
102
+ open = false;
103
+ }
104
+
105
+ // The centred dialog fades in place; the full-screen one slides up like
106
+ // a sheet. Both at the dialog timing; reduced motion fades both.
107
+ function panel(node: Element) {
108
+ return form === 'fullscreen' ? sheet(node) : fade(node);
109
+ }
110
+ </script>
111
+
112
+ <BitsDialog.Root bind:open>
113
+ {#if trigger}
114
+ <BitsDialog.Trigger>
115
+ {#snippet child({ props })}
116
+ {@render trigger({ props })}
117
+ {/snippet}
118
+ </BitsDialog.Trigger>
119
+ {/if}
120
+
121
+ <BitsDialog.Portal>
122
+ <BitsDialog.Content
123
+ forceMount
124
+ bind:ref
125
+ aria-labelledby={titleId}
126
+ aria-describedby={description ? descriptionId : undefined}
127
+ interactOutsideBehavior={fullscreen ? 'ignore' : 'close'}
128
+ {...rest}
129
+ >
130
+ {#snippet child({ props: { interactOutsideBehavior: _, ...props }, open: shown })}
131
+ {#if form === 'sheet'}
132
+ <Sheet
133
+ {...props}
134
+ class={['hp-dialog', className]}
135
+ data-form="sheet"
136
+ open={shown}
137
+ layer="dialog"
138
+ {title}
139
+ {titleId}
140
+ titleTag="h2"
141
+ {closeLabel}
142
+ onclose={close}
143
+ footer={actions ? actionRow : undefined}
144
+ >
145
+ {@render body()}
146
+ </Sheet>
147
+ {:else}
148
+ <Scrim open={shown} layer="dialog" />
149
+ {#if shown}
150
+ <div
151
+ {...props}
152
+ class={['hp-dialog', className]}
153
+ data-form={form}
154
+ style:top={lifted ? `${lifted.top}px` : undefined}
155
+ style:bottom={lifted ? 'auto' : undefined}
156
+ style:height={lifted ? `${lifted.height}px` : undefined}
157
+ style:padding-bottom={lifted ? '0px' : undefined}
158
+ transition:panel
159
+ >
160
+ <div class="hp-dialog-head" data-form={form}>
161
+ <h2 id={titleId} class="hp-dialog-title hp-view-title">{title}</h2>
162
+ <button type="button" class="hp-dialog-close" aria-label={closeLabel} onclick={close}>
163
+ <svg
164
+ viewBox="0 0 16 16"
165
+ fill="none"
166
+ stroke="currentColor"
167
+ stroke-width="1.5"
168
+ stroke-linecap="square"
169
+ aria-hidden="true"
170
+ >
171
+ <path d="M3 3l10 10M13 3L3 13" />
172
+ </svg>
173
+ </button>
174
+ </div>
175
+ {@render body()}
176
+ {#if actions}
177
+ {@render actionRow()}
178
+ {/if}
179
+ </div>
180
+ {/if}
181
+ {/if}
182
+ {/snippet}
183
+ </BitsDialog.Content>
184
+ </BitsDialog.Portal>
185
+ </BitsDialog.Root>
186
+
187
+ {#snippet body()}
188
+ <div
189
+ class="hp-dialog-body"
190
+ data-form={form}
191
+ data-end={actions ? undefined : ''}
192
+ bind:this={bodyNode}
193
+ >
194
+ {#if description}
195
+ <p id={descriptionId} class="hp-dialog-description">{description}</p>
196
+ {/if}
197
+ {@render children?.()}
198
+ </div>
199
+ {/snippet}
200
+
201
+ {#snippet actionRow()}
202
+ <div class="hp-dialog-actions" data-form={form}>
203
+ {@render actions?.({ close })}
204
+ </div>
205
+ {/snippet}
206
+
207
+ <style>
208
+ /* ---- Centred (600px and up) and full screen (compact) --------------- */
209
+
210
+ .hp-dialog[data-form='centred'],
211
+ .hp-dialog[data-form='fullscreen'] {
212
+ position: fixed;
213
+ z-index: var(--layer-dialog);
214
+ display: flex;
215
+ flex-direction: column;
216
+ box-sizing: border-box;
217
+ background: var(--canvas);
218
+ color: var(--text);
219
+ overscroll-behavior: contain;
220
+ /* Bits UI may have turned pointer events off on <body>. */
221
+ pointer-events: auto;
222
+ }
223
+
224
+ /* A square black panel with a 3:1 boundary, centred in the safe area. */
225
+ .hp-dialog[data-form='centred'] {
226
+ inset: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
227
+ env(safe-area-inset-left);
228
+ width: min(560px, 100% - 2 * var(--gutter));
229
+ height: fit-content;
230
+ max-height: calc(100dvh - env(safe-area-inset-top) - env(safe-area-inset-bottom) - 2 * var(--space-8));
231
+ margin: auto;
232
+ border: 1px solid var(--border-control);
233
+ }
234
+
235
+ /* The whole screen, inside the safe areas. While the on-screen keyboard
236
+ is up, the visual viewport above it, without the bottom safe area,
237
+ which the keyboard covers (set inline). */
238
+ .hp-dialog[data-form='fullscreen'] {
239
+ inset: 0;
240
+ padding: env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom)
241
+ env(safe-area-inset-left);
242
+ }
243
+
244
+ /* Title row: the caps title, then the close button at the end. A long
245
+ title wraps (200 percent zoom) and the button stays on its first line:
246
+ the title's padding centres that line on the button, so a single line
247
+ sits where the row centred it before. The title's clip stays: it keeps
248
+ the first glyph's antialiasing inside the text edge, as before. */
249
+ .hp-dialog-head {
250
+ flex: none;
251
+ display: grid;
252
+ grid-template-columns: minmax(0, 1fr) auto;
253
+ align-items: start;
254
+ gap: var(--space-2);
255
+ }
256
+ .hp-dialog-head[data-form='centred'] {
257
+ padding: var(--space-4) var(--space-4) 0 var(--space-6);
258
+ }
259
+ /* The shell header's height, so the form reads as its own view. */
260
+ .hp-dialog-head[data-form='fullscreen'] {
261
+ min-height: var(--header-height);
262
+ padding: 0 var(--space-1) 0 var(--gutter);
263
+ }
264
+ .hp-dialog-title {
265
+ overflow: hidden;
266
+ padding-block: calc((var(--control-height) - 1lh) / 2);
267
+ overflow-wrap: anywhere;
268
+ }
269
+ .hp-dialog-head[data-form='fullscreen'] .hp-dialog-title {
270
+ padding-block: calc((var(--header-height) - 1lh) / 2);
271
+ }
272
+ .hp-dialog-head[data-form='fullscreen'] .hp-dialog-close {
273
+ margin-block: calc((var(--header-height) - var(--control-height)) / 2);
274
+ }
275
+
276
+ .hp-dialog-close {
277
+ display: grid;
278
+ place-items: center;
279
+ /* 36px on fine pointers, 44px on coarse. */
280
+ width: var(--control-height);
281
+ height: var(--control-height);
282
+ padding: 0;
283
+ border: 0;
284
+ background: transparent;
285
+ color: var(--text);
286
+ cursor: pointer;
287
+ -webkit-tap-highlight-color: transparent;
288
+ }
289
+ .hp-dialog-close svg {
290
+ width: var(--space-4);
291
+ height: var(--space-4);
292
+ }
293
+ .hp-dialog-close:active {
294
+ background: var(--surface-raised);
295
+ }
296
+ @media (hover: hover) and (pointer: fine) {
297
+ .hp-dialog-close {
298
+ transition: background-color var(--dur-hover) var(--settle);
299
+ }
300
+ .hp-dialog-close:hover {
301
+ background: var(--surface-hover);
302
+ }
303
+ .hp-dialog-close:active {
304
+ background: var(--surface-raised);
305
+ transition-duration: var(--dur-press);
306
+ }
307
+ }
308
+
309
+ /* ---- Body and actions, in every form -------------------------------- */
310
+
311
+ /* The top padding keeps a first field's focus contour (4px out, 2px
312
+ wide) inside the scrolling box. The scroll padding keeps a focused
313
+ field's caret 8px clear of the action row when the browser reveals it,
314
+ as when a TextArea grows while someone types. */
315
+ .hp-dialog-body {
316
+ display: grid;
317
+ align-content: start;
318
+ gap: var(--space-4);
319
+ min-width: 0;
320
+ padding: var(--space-2) var(--gutter);
321
+ scroll-padding-block: var(--space-2);
322
+ font-size: var(--type-body);
323
+ line-height: 24px;
324
+ }
325
+ /* The sheet's body scrolls itself; these scroll inside the panel. */
326
+ .hp-dialog-body[data-form='centred'],
327
+ .hp-dialog-body[data-form='fullscreen'] {
328
+ flex: 1 1 auto;
329
+ min-height: 0;
330
+ overflow-y: auto;
331
+ overscroll-behavior: contain;
332
+ }
333
+ .hp-dialog-body[data-form='centred'] {
334
+ padding-inline: var(--space-6);
335
+ }
336
+ /* Without an action row the body ends the panel. */
337
+ .hp-dialog-body[data-end] {
338
+ padding-bottom: var(--space-4);
339
+ }
340
+ .hp-dialog-body[data-form='centred'][data-end] {
341
+ padding-bottom: var(--space-6);
342
+ }
343
+ .hp-dialog-description {
344
+ color: var(--muted);
345
+ }
346
+
347
+ /* Actions sit at the end, primary last, 8px apart. */
348
+ .hp-dialog-actions {
349
+ flex: none;
350
+ display: flex;
351
+ flex-wrap: wrap;
352
+ justify-content: flex-end;
353
+ gap: var(--space-2);
354
+ padding: var(--space-4) var(--gutter);
355
+ }
356
+ .hp-dialog-actions[data-form='centred'] {
357
+ padding: var(--space-6);
358
+ }
359
+ </style>