@mystravil/hardpoint 0.1.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.
- package/CHANGELOG.md +42 -18
- package/LICENSE +25 -25
- package/README.md +67 -64
- package/dist/base.css +625 -346
- package/dist/components/AppShell.svelte +698 -698
- package/dist/components/Button.svelte +174 -174
- package/dist/components/Checkbox.svelte +235 -198
- package/dist/components/Checkbox.svelte.d.ts +9 -2
- package/dist/components/CheckboxGroup.svelte +207 -99
- package/dist/components/CheckboxGroup.svelte.d.ts +23 -4
- package/dist/components/Combobox.svelte +901 -824
- package/dist/components/Combobox.svelte.d.ts +10 -1
- package/dist/components/Dialog.svelte +359 -356
- package/dist/components/Disclosure.svelte +201 -201
- package/dist/components/EmptyState.svelte +88 -88
- package/dist/components/ErrorState.svelte +135 -135
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -114
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -219
- package/dist/components/MediaFrame.svelte +110 -110
- package/dist/components/MediaImage.svelte +256 -256
- package/dist/components/Menu.svelte +258 -258
- package/dist/components/MenuCheckboxItem.svelte +119 -119
- package/dist/components/MenuGroup.svelte +50 -50
- package/dist/components/MenuItem.svelte +94 -94
- package/dist/components/MenuRadioGroup.svelte +62 -62
- package/dist/components/MenuRadioItem.svelte +110 -110
- package/dist/components/MenuSeparator.svelte +28 -28
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +18 -2
- package/dist/components/NativeSelect.svelte.d.ts +6 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Popover.svelte +222 -222
- package/dist/components/Progress.svelte +157 -157
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -405
- package/dist/components/Select.svelte.d.ts +6 -0
- package/dist/components/Skeleton.svelte +149 -149
- package/dist/components/Spinner.svelte +118 -117
- package/dist/components/Stat.svelte +253 -253
- package/dist/components/Steps.svelte +189 -189
- package/dist/components/Switch.svelte +222 -222
- package/dist/components/Table.svelte +504 -504
- package/dist/components/Tabs.svelte +246 -246
- package/dist/components/Tag.svelte +40 -40
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -256
- package/dist/components/Timeline.svelte +156 -156
- package/dist/components/Toaster.svelte +411 -411
- package/dist/components/Tooltip.svelte +200 -200
- package/dist/components/TooltipGroup.svelte +46 -46
- package/dist/components/internal/Scrim.svelte +52 -52
- package/dist/components/internal/Sheet.svelte +263 -263
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/fonts.css +71 -71
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/tokens.css +283 -279
- package/package.json +81 -76
|
@@ -1,263 +1,263 @@
|
|
|
1
|
-
<script lang="ts" module>
|
|
2
|
-
import type { Snippet } from 'svelte';
|
|
3
|
-
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
-
import type { OverlayLayer } from './Scrim.svelte';
|
|
5
|
-
|
|
6
|
-
/** Attributes and handlers not listed here go to the panel. */
|
|
7
|
-
export type SheetProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
|
|
8
|
-
/**
|
|
9
|
-
* Shown while true. Pass the `open` that the Bits UI content's
|
|
10
|
-
* `child` snippet receives (render the content with `forceMount`), so
|
|
11
|
-
* the sheet and its scrim slide and fade out before they unmount.
|
|
12
|
-
*/
|
|
13
|
-
open: boolean;
|
|
14
|
-
/** The title row's text, in tracked caps. */
|
|
15
|
-
title: string;
|
|
16
|
-
/** The title element's `id`, so the content can be labelled by it. */
|
|
17
|
-
titleId?: string;
|
|
18
|
-
/** The title element: `p`, or `h2` when the sheet is a dialog. */
|
|
19
|
-
titleTag?: 'p' | 'h2';
|
|
20
|
-
/** The z-index token: `dialog` for dialogs, `popover` for everything else. */
|
|
21
|
-
layer: OverlayLayer;
|
|
22
|
-
/**
|
|
23
|
-
* Called by the close button and by a swipe down. A tap on the scrim
|
|
24
|
-
* and Escape close through Bits UI.
|
|
25
|
-
*/
|
|
26
|
-
onclose: () => void;
|
|
27
|
-
/** The close button's accessible name. */
|
|
28
|
-
closeLabel?: string;
|
|
29
|
-
/** The panel element. */
|
|
30
|
-
ref?: HTMLElement | null;
|
|
31
|
-
/** The body, under the title row. It scrolls if it does not fit. */
|
|
32
|
-
children: Snippet;
|
|
33
|
-
/** A row pinned under the body, such as a dialog's actions. */
|
|
34
|
-
footer?: Snippet;
|
|
35
|
-
};
|
|
36
|
-
</script>
|
|
37
|
-
|
|
38
|
-
<script lang="ts">
|
|
39
|
-
import { sheet } from '../../motion/index.js';
|
|
40
|
-
import Scrim from './Scrim.svelte';
|
|
41
|
-
import { swipeDismiss } from './swipe-dismiss.js';
|
|
42
|
-
import { followVisualViewport } from './visual-viewport.svelte.js';
|
|
43
|
-
|
|
44
|
-
let {
|
|
45
|
-
open,
|
|
46
|
-
title,
|
|
47
|
-
titleId,
|
|
48
|
-
titleTag = 'p',
|
|
49
|
-
layer,
|
|
50
|
-
onclose,
|
|
51
|
-
closeLabel = 'Close',
|
|
52
|
-
ref = $bindable(null),
|
|
53
|
-
children,
|
|
54
|
-
footer,
|
|
55
|
-
class: className,
|
|
56
|
-
...rest
|
|
57
|
-
}: SheetProps = $props();
|
|
58
|
-
|
|
59
|
-
let body = $state<HTMLElement | null>(null);
|
|
60
|
-
|
|
61
|
-
// The sheet follows the visual viewport while it is open (decided
|
|
62
|
-
// 2026-11-10; internal/visual-viewport.svelte.ts): while the on-screen
|
|
63
|
-
// keyboard shortens it, the sheet's bottom edge sits on its bottom edge
|
|
64
|
-
// and the sheet may grow to its height less the header row's
|
|
65
|
-
// (--header-height and the top safe area), where it was half the screen.
|
|
66
|
-
// The focused field in the body stays in sight.
|
|
67
|
-
const viewport = followVisualViewport(() => open, () => body);
|
|
68
|
-
|
|
69
|
-
// The press that opened the sheet never acts on what the sheet now shows
|
|
70
|
-
// under the finger (decided 2026-11-10). On touch, Bits UI's Menu and
|
|
71
|
-
// Select triggers open on pointerup, and the click that follows lands on
|
|
72
|
-
// the sheet, which under reduced motion fades in where it ends up; a
|
|
73
|
-
// mouse opens them on pointerdown, and its pointerup on an item counts
|
|
74
|
-
// as a choice. So until a press starts inside the sheet, a pointerup or a
|
|
75
|
-
// pointer's click inside it stops here. A click from Enter, Space or
|
|
76
|
-
// element.click() has detail 0 and always passes.
|
|
77
|
-
function ignoreOpeningPress(node: HTMLElement) {
|
|
78
|
-
let pressed = false;
|
|
79
|
-
const press = () => (pressed = true);
|
|
80
|
-
const guard = (e: MouseEvent) => {
|
|
81
|
-
if (pressed || (e.type === 'click' && e.detail === 0)) return;
|
|
82
|
-
e.preventDefault();
|
|
83
|
-
e.stopImmediatePropagation();
|
|
84
|
-
};
|
|
85
|
-
const capture = { capture: true };
|
|
86
|
-
node.addEventListener('pointerdown', press, capture);
|
|
87
|
-
node.addEventListener('pointerup', guard, capture);
|
|
88
|
-
node.addEventListener('click', guard, capture);
|
|
89
|
-
return () => {
|
|
90
|
-
node.removeEventListener('pointerdown', press, capture);
|
|
91
|
-
node.removeEventListener('pointerup', guard, capture);
|
|
92
|
-
node.removeEventListener('click', guard, capture);
|
|
93
|
-
};
|
|
94
|
-
}
|
|
95
|
-
</script>
|
|
96
|
-
|
|
97
|
-
<!--
|
|
98
|
-
The bottom sheet every overlay uses on compact widths (BRIEF, Apps and
|
|
99
|
-
phones: Overlays): a scrim, then a panel at the bottom edge with a handle,
|
|
100
|
-
a caps title, a close button, the body and an optional footer. It opens at
|
|
101
|
-
half the screen at most, with no drag to expand; the body scrolls inside.
|
|
102
|
-
A swipe down dismisses it (internal/swipe-dismiss.ts). While the visual
|
|
103
|
-
viewport is shorter than the layout viewport (the on-screen keyboard), it
|
|
104
|
-
sits on top of it and may fill the space above it. The press that opened
|
|
105
|
-
it does not act inside it.
|
|
106
|
-
|
|
107
|
-
Render it inside the Bits UI content's `child` snippet and spread the
|
|
108
|
-
snippet's `props` onto it when the panel is the content element (a
|
|
109
|
-
dialog), or onto an element in the body when the content is a list (a
|
|
110
|
-
menu, a select): then pass `onInteractOutside` to the content and call
|
|
111
|
-
`preventDefault()` for presses inside `ref`, so the title row does not
|
|
112
|
-
count as outside.
|
|
113
|
-
-->
|
|
114
|
-
<Scrim {open} {layer} />
|
|
115
|
-
{#if open}
|
|
116
|
-
{@const lifted = viewport.current}
|
|
117
|
-
<div
|
|
118
|
-
bind:this={ref}
|
|
119
|
-
{...rest}
|
|
120
|
-
class={['hp-sheet', className]}
|
|
121
|
-
data-layer={layer}
|
|
122
|
-
style:bottom={lifted ? `calc(100% - ${lifted.bottom}px)` : undefined}
|
|
123
|
-
style:padding-bottom={lifted ? '0px' : undefined}
|
|
124
|
-
style:--sheet-max-height={lifted
|
|
125
|
-
? `calc(${lifted.height}px - var(--header-height) - env(safe-area-inset-top))`
|
|
126
|
-
: undefined}
|
|
127
|
-
transition:sheet
|
|
128
|
-
{@attach ignoreOpeningPress}
|
|
129
|
-
{@attach swipeDismiss(() => onclose())}
|
|
130
|
-
>
|
|
131
|
-
<div class="hp-sheet-head">
|
|
132
|
-
<span class="hp-sheet-handle" aria-hidden="true"></span>
|
|
133
|
-
<svelte:element this={titleTag} id={titleId} class="hp-sheet-title">{title}</svelte:element>
|
|
134
|
-
<button type="button" class="hp-sheet-close" aria-label={closeLabel} onclick={() => onclose()}>
|
|
135
|
-
<svg
|
|
136
|
-
viewBox="0 0 16 16"
|
|
137
|
-
fill="none"
|
|
138
|
-
stroke="currentColor"
|
|
139
|
-
stroke-width="1.5"
|
|
140
|
-
stroke-linecap="square"
|
|
141
|
-
aria-hidden="true"
|
|
142
|
-
>
|
|
143
|
-
<path d="M3 3l10 10M13 3L3 13" />
|
|
144
|
-
</svg>
|
|
145
|
-
</button>
|
|
146
|
-
</div>
|
|
147
|
-
<div class="hp-sheet-body" bind:this={body}>
|
|
148
|
-
{@render children()}
|
|
149
|
-
</div>
|
|
150
|
-
{#if footer}
|
|
151
|
-
<div class="hp-sheet-footer">
|
|
152
|
-
{@render footer()}
|
|
153
|
-
</div>
|
|
154
|
-
{/if}
|
|
155
|
-
</div>
|
|
156
|
-
{/if}
|
|
157
|
-
|
|
158
|
-
<style>
|
|
159
|
-
/* While a floating list is open Bits UI sets pointer-events: none on
|
|
160
|
-
<body>; the panel takes presses again. */
|
|
161
|
-
.hp-sheet {
|
|
162
|
-
/* The height cap: half the screen, or the room above the on-screen
|
|
163
|
-
keyboard while it is up (set inline). A list that must keep its
|
|
164
|
-
height while it filters asks for this. */
|
|
165
|
-
--sheet-max-height: 50dvh;
|
|
166
|
-
|
|
167
|
-
position: fixed;
|
|
168
|
-
inset: auto 0 0;
|
|
169
|
-
z-index: var(--layer-dialog);
|
|
170
|
-
display: flex;
|
|
171
|
-
flex-direction: column;
|
|
172
|
-
box-sizing: border-box;
|
|
173
|
-
/* Opens at half the screen at most; the body scrolls inside. */
|
|
174
|
-
max-height: var(--sheet-max-height);
|
|
175
|
-
padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
176
|
-
border-top: 1px solid var(--border-control);
|
|
177
|
-
background: var(--canvas);
|
|
178
|
-
color: var(--text);
|
|
179
|
-
overscroll-behavior: contain;
|
|
180
|
-
pointer-events: auto;
|
|
181
|
-
}
|
|
182
|
-
.hp-sheet[data-layer='popover'] {
|
|
183
|
-
z-index: var(--layer-popover);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/* The grab area: handle, title and close button. The swipe owns touches
|
|
187
|
-
here; the body below keeps its scroll until it reaches the top. */
|
|
188
|
-
.hp-sheet-head {
|
|
189
|
-
flex: none;
|
|
190
|
-
display: grid;
|
|
191
|
-
grid-template-columns: minmax(0, 1fr) auto;
|
|
192
|
-
align-items: start;
|
|
193
|
-
padding: 0 var(--space-1) 0 var(--gutter);
|
|
194
|
-
touch-action: none;
|
|
195
|
-
}
|
|
196
|
-
.hp-sheet-handle {
|
|
197
|
-
grid-column: 1 / -1;
|
|
198
|
-
justify-self: center;
|
|
199
|
-
width: var(--space-8);
|
|
200
|
-
height: var(--space-1);
|
|
201
|
-
margin-top: var(--space-2);
|
|
202
|
-
background: var(--border-control);
|
|
203
|
-
}
|
|
204
|
-
/* Tracked caps at the compact view-title size. A long title wraps (200
|
|
205
|
-
percent zoom); the padding centres its first line on the 44px close
|
|
206
|
-
button, so a single line sits where it always did. The clip stays: it
|
|
207
|
-
keeps the first glyph's antialiasing inside the text edge, as before. */
|
|
208
|
-
.hp-sheet-title {
|
|
209
|
-
overflow: hidden;
|
|
210
|
-
padding-block: calc((44px - 1lh) / 2);
|
|
211
|
-
font-size: var(--type-body);
|
|
212
|
-
font-weight: 500;
|
|
213
|
-
line-height: 24px;
|
|
214
|
-
letter-spacing: var(--label-tracking);
|
|
215
|
-
text-transform: var(--label-transform);
|
|
216
|
-
overflow-wrap: anywhere;
|
|
217
|
-
}
|
|
218
|
-
.hp-sheet-close {
|
|
219
|
-
display: grid;
|
|
220
|
-
place-items: center;
|
|
221
|
-
width: 44px;
|
|
222
|
-
height: 44px;
|
|
223
|
-
padding: 0;
|
|
224
|
-
border: 0;
|
|
225
|
-
background: transparent;
|
|
226
|
-
color: var(--text);
|
|
227
|
-
cursor: pointer;
|
|
228
|
-
-webkit-tap-highlight-color: transparent;
|
|
229
|
-
}
|
|
230
|
-
.hp-sheet-close svg {
|
|
231
|
-
width: var(--space-4);
|
|
232
|
-
height: var(--space-4);
|
|
233
|
-
}
|
|
234
|
-
.hp-sheet-close:active {
|
|
235
|
-
background: var(--surface-raised);
|
|
236
|
-
}
|
|
237
|
-
@media (hover: hover) and (pointer: fine) {
|
|
238
|
-
.hp-sheet-close {
|
|
239
|
-
transition: background-color var(--dur-hover) var(--settle);
|
|
240
|
-
}
|
|
241
|
-
.hp-sheet-close:hover {
|
|
242
|
-
background: var(--surface-hover);
|
|
243
|
-
}
|
|
244
|
-
.hp-sheet-close:active {
|
|
245
|
-
background: var(--surface-raised);
|
|
246
|
-
transition-duration: var(--dur-press);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/* A flex column, so a list inside can take the rest of the height
|
|
251
|
-
(flex: 1 1 auto; min-height: 0) and scroll on its own. */
|
|
252
|
-
.hp-sheet-body {
|
|
253
|
-
display: flex;
|
|
254
|
-
flex-direction: column;
|
|
255
|
-
flex: 1 1 auto;
|
|
256
|
-
min-height: 0;
|
|
257
|
-
overflow-y: auto;
|
|
258
|
-
overscroll-behavior: contain;
|
|
259
|
-
}
|
|
260
|
-
.hp-sheet-footer {
|
|
261
|
-
flex: none;
|
|
262
|
-
}
|
|
263
|
-
</style>
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
3
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
|
+
import type { OverlayLayer } from './Scrim.svelte';
|
|
5
|
+
|
|
6
|
+
/** Attributes and handlers not listed here go to the panel. */
|
|
7
|
+
export type SheetProps = Omit<HTMLAttributes<HTMLDivElement>, 'title' | 'children'> & {
|
|
8
|
+
/**
|
|
9
|
+
* Shown while true. Pass the `open` that the Bits UI content's
|
|
10
|
+
* `child` snippet receives (render the content with `forceMount`), so
|
|
11
|
+
* the sheet and its scrim slide and fade out before they unmount.
|
|
12
|
+
*/
|
|
13
|
+
open: boolean;
|
|
14
|
+
/** The title row's text, in tracked caps. */
|
|
15
|
+
title: string;
|
|
16
|
+
/** The title element's `id`, so the content can be labelled by it. */
|
|
17
|
+
titleId?: string;
|
|
18
|
+
/** The title element: `p`, or `h2` when the sheet is a dialog. */
|
|
19
|
+
titleTag?: 'p' | 'h2';
|
|
20
|
+
/** The z-index token: `dialog` for dialogs, `popover` for everything else. */
|
|
21
|
+
layer: OverlayLayer;
|
|
22
|
+
/**
|
|
23
|
+
* Called by the close button and by a swipe down. A tap on the scrim
|
|
24
|
+
* and Escape close through Bits UI.
|
|
25
|
+
*/
|
|
26
|
+
onclose: () => void;
|
|
27
|
+
/** The close button's accessible name. */
|
|
28
|
+
closeLabel?: string;
|
|
29
|
+
/** The panel element. */
|
|
30
|
+
ref?: HTMLElement | null;
|
|
31
|
+
/** The body, under the title row. It scrolls if it does not fit. */
|
|
32
|
+
children: Snippet;
|
|
33
|
+
/** A row pinned under the body, such as a dialog's actions. */
|
|
34
|
+
footer?: Snippet;
|
|
35
|
+
};
|
|
36
|
+
</script>
|
|
37
|
+
|
|
38
|
+
<script lang="ts">
|
|
39
|
+
import { sheet } from '../../motion/index.js';
|
|
40
|
+
import Scrim from './Scrim.svelte';
|
|
41
|
+
import { swipeDismiss } from './swipe-dismiss.js';
|
|
42
|
+
import { followVisualViewport } from './visual-viewport.svelte.js';
|
|
43
|
+
|
|
44
|
+
let {
|
|
45
|
+
open,
|
|
46
|
+
title,
|
|
47
|
+
titleId,
|
|
48
|
+
titleTag = 'p',
|
|
49
|
+
layer,
|
|
50
|
+
onclose,
|
|
51
|
+
closeLabel = 'Close',
|
|
52
|
+
ref = $bindable(null),
|
|
53
|
+
children,
|
|
54
|
+
footer,
|
|
55
|
+
class: className,
|
|
56
|
+
...rest
|
|
57
|
+
}: SheetProps = $props();
|
|
58
|
+
|
|
59
|
+
let body = $state<HTMLElement | null>(null);
|
|
60
|
+
|
|
61
|
+
// The sheet follows the visual viewport while it is open (decided
|
|
62
|
+
// 2026-11-10; internal/visual-viewport.svelte.ts): while the on-screen
|
|
63
|
+
// keyboard shortens it, the sheet's bottom edge sits on its bottom edge
|
|
64
|
+
// and the sheet may grow to its height less the header row's
|
|
65
|
+
// (--header-height and the top safe area), where it was half the screen.
|
|
66
|
+
// The focused field in the body stays in sight.
|
|
67
|
+
const viewport = followVisualViewport(() => open, () => body);
|
|
68
|
+
|
|
69
|
+
// The press that opened the sheet never acts on what the sheet now shows
|
|
70
|
+
// under the finger (decided 2026-11-10). On touch, Bits UI's Menu and
|
|
71
|
+
// Select triggers open on pointerup, and the click that follows lands on
|
|
72
|
+
// the sheet, which under reduced motion fades in where it ends up; a
|
|
73
|
+
// mouse opens them on pointerdown, and its pointerup on an item counts
|
|
74
|
+
// as a choice. So until a press starts inside the sheet, a pointerup or a
|
|
75
|
+
// pointer's click inside it stops here. A click from Enter, Space or
|
|
76
|
+
// element.click() has detail 0 and always passes.
|
|
77
|
+
function ignoreOpeningPress(node: HTMLElement) {
|
|
78
|
+
let pressed = false;
|
|
79
|
+
const press = () => (pressed = true);
|
|
80
|
+
const guard = (e: MouseEvent) => {
|
|
81
|
+
if (pressed || (e.type === 'click' && e.detail === 0)) return;
|
|
82
|
+
e.preventDefault();
|
|
83
|
+
e.stopImmediatePropagation();
|
|
84
|
+
};
|
|
85
|
+
const capture = { capture: true };
|
|
86
|
+
node.addEventListener('pointerdown', press, capture);
|
|
87
|
+
node.addEventListener('pointerup', guard, capture);
|
|
88
|
+
node.addEventListener('click', guard, capture);
|
|
89
|
+
return () => {
|
|
90
|
+
node.removeEventListener('pointerdown', press, capture);
|
|
91
|
+
node.removeEventListener('pointerup', guard, capture);
|
|
92
|
+
node.removeEventListener('click', guard, capture);
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
</script>
|
|
96
|
+
|
|
97
|
+
<!--
|
|
98
|
+
The bottom sheet every overlay uses on compact widths (BRIEF, Apps and
|
|
99
|
+
phones: Overlays): a scrim, then a panel at the bottom edge with a handle,
|
|
100
|
+
a caps title, a close button, the body and an optional footer. It opens at
|
|
101
|
+
half the screen at most, with no drag to expand; the body scrolls inside.
|
|
102
|
+
A swipe down dismisses it (internal/swipe-dismiss.ts). While the visual
|
|
103
|
+
viewport is shorter than the layout viewport (the on-screen keyboard), it
|
|
104
|
+
sits on top of it and may fill the space above it. The press that opened
|
|
105
|
+
it does not act inside it.
|
|
106
|
+
|
|
107
|
+
Render it inside the Bits UI content's `child` snippet and spread the
|
|
108
|
+
snippet's `props` onto it when the panel is the content element (a
|
|
109
|
+
dialog), or onto an element in the body when the content is a list (a
|
|
110
|
+
menu, a select): then pass `onInteractOutside` to the content and call
|
|
111
|
+
`preventDefault()` for presses inside `ref`, so the title row does not
|
|
112
|
+
count as outside.
|
|
113
|
+
-->
|
|
114
|
+
<Scrim {open} {layer} />
|
|
115
|
+
{#if open}
|
|
116
|
+
{@const lifted = viewport.current}
|
|
117
|
+
<div
|
|
118
|
+
bind:this={ref}
|
|
119
|
+
{...rest}
|
|
120
|
+
class={['hp-sheet', className]}
|
|
121
|
+
data-layer={layer}
|
|
122
|
+
style:bottom={lifted ? `calc(100% - ${lifted.bottom}px)` : undefined}
|
|
123
|
+
style:padding-bottom={lifted ? '0px' : undefined}
|
|
124
|
+
style:--sheet-max-height={lifted
|
|
125
|
+
? `calc(${lifted.height}px - var(--header-height) - env(safe-area-inset-top))`
|
|
126
|
+
: undefined}
|
|
127
|
+
transition:sheet
|
|
128
|
+
{@attach ignoreOpeningPress}
|
|
129
|
+
{@attach swipeDismiss(() => onclose())}
|
|
130
|
+
>
|
|
131
|
+
<div class="hp-sheet-head">
|
|
132
|
+
<span class="hp-sheet-handle" aria-hidden="true"></span>
|
|
133
|
+
<svelte:element this={titleTag} id={titleId} class="hp-sheet-title">{title}</svelte:element>
|
|
134
|
+
<button type="button" class="hp-sheet-close" aria-label={closeLabel} onclick={() => onclose()}>
|
|
135
|
+
<svg
|
|
136
|
+
viewBox="0 0 16 16"
|
|
137
|
+
fill="none"
|
|
138
|
+
stroke="currentColor"
|
|
139
|
+
stroke-width="1.5"
|
|
140
|
+
stroke-linecap="square"
|
|
141
|
+
aria-hidden="true"
|
|
142
|
+
>
|
|
143
|
+
<path d="M3 3l10 10M13 3L3 13" />
|
|
144
|
+
</svg>
|
|
145
|
+
</button>
|
|
146
|
+
</div>
|
|
147
|
+
<div class="hp-sheet-body" bind:this={body}>
|
|
148
|
+
{@render children()}
|
|
149
|
+
</div>
|
|
150
|
+
{#if footer}
|
|
151
|
+
<div class="hp-sheet-footer">
|
|
152
|
+
{@render footer()}
|
|
153
|
+
</div>
|
|
154
|
+
{/if}
|
|
155
|
+
</div>
|
|
156
|
+
{/if}
|
|
157
|
+
|
|
158
|
+
<style>
|
|
159
|
+
/* While a floating list is open Bits UI sets pointer-events: none on
|
|
160
|
+
<body>; the panel takes presses again. */
|
|
161
|
+
.hp-sheet {
|
|
162
|
+
/* The height cap: half the screen, or the room above the on-screen
|
|
163
|
+
keyboard while it is up (set inline). A list that must keep its
|
|
164
|
+
height while it filters asks for this. */
|
|
165
|
+
--sheet-max-height: 50dvh;
|
|
166
|
+
|
|
167
|
+
position: fixed;
|
|
168
|
+
inset: auto 0 0;
|
|
169
|
+
z-index: var(--layer-dialog);
|
|
170
|
+
display: flex;
|
|
171
|
+
flex-direction: column;
|
|
172
|
+
box-sizing: border-box;
|
|
173
|
+
/* Opens at half the screen at most; the body scrolls inside. */
|
|
174
|
+
max-height: var(--sheet-max-height);
|
|
175
|
+
padding: 0 env(safe-area-inset-right) env(safe-area-inset-bottom) env(safe-area-inset-left);
|
|
176
|
+
border-top: 1px solid var(--border-control);
|
|
177
|
+
background: var(--canvas);
|
|
178
|
+
color: var(--text);
|
|
179
|
+
overscroll-behavior: contain;
|
|
180
|
+
pointer-events: auto;
|
|
181
|
+
}
|
|
182
|
+
.hp-sheet[data-layer='popover'] {
|
|
183
|
+
z-index: var(--layer-popover);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* The grab area: handle, title and close button. The swipe owns touches
|
|
187
|
+
here; the body below keeps its scroll until it reaches the top. */
|
|
188
|
+
.hp-sheet-head {
|
|
189
|
+
flex: none;
|
|
190
|
+
display: grid;
|
|
191
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
192
|
+
align-items: start;
|
|
193
|
+
padding: 0 var(--space-1) 0 var(--gutter);
|
|
194
|
+
touch-action: none;
|
|
195
|
+
}
|
|
196
|
+
.hp-sheet-handle {
|
|
197
|
+
grid-column: 1 / -1;
|
|
198
|
+
justify-self: center;
|
|
199
|
+
width: var(--space-8);
|
|
200
|
+
height: var(--space-1);
|
|
201
|
+
margin-top: var(--space-2);
|
|
202
|
+
background: var(--border-control);
|
|
203
|
+
}
|
|
204
|
+
/* Tracked caps at the compact view-title size. A long title wraps (200
|
|
205
|
+
percent zoom); the padding centres its first line on the 44px close
|
|
206
|
+
button, so a single line sits where it always did. The clip stays: it
|
|
207
|
+
keeps the first glyph's antialiasing inside the text edge, as before. */
|
|
208
|
+
.hp-sheet-title {
|
|
209
|
+
overflow: hidden;
|
|
210
|
+
padding-block: calc((44px - 1lh) / 2);
|
|
211
|
+
font-size: var(--type-body);
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
line-height: 24px;
|
|
214
|
+
letter-spacing: var(--label-tracking);
|
|
215
|
+
text-transform: var(--label-transform);
|
|
216
|
+
overflow-wrap: anywhere;
|
|
217
|
+
}
|
|
218
|
+
.hp-sheet-close {
|
|
219
|
+
display: grid;
|
|
220
|
+
place-items: center;
|
|
221
|
+
width: 44px;
|
|
222
|
+
height: 44px;
|
|
223
|
+
padding: 0;
|
|
224
|
+
border: 0;
|
|
225
|
+
background: transparent;
|
|
226
|
+
color: var(--text);
|
|
227
|
+
cursor: pointer;
|
|
228
|
+
-webkit-tap-highlight-color: transparent;
|
|
229
|
+
}
|
|
230
|
+
.hp-sheet-close svg {
|
|
231
|
+
width: var(--space-4);
|
|
232
|
+
height: var(--space-4);
|
|
233
|
+
}
|
|
234
|
+
.hp-sheet-close:active {
|
|
235
|
+
background: var(--surface-raised);
|
|
236
|
+
}
|
|
237
|
+
@media (hover: hover) and (pointer: fine) {
|
|
238
|
+
.hp-sheet-close {
|
|
239
|
+
transition: background-color var(--dur-hover) var(--settle);
|
|
240
|
+
}
|
|
241
|
+
.hp-sheet-close:hover {
|
|
242
|
+
background: var(--surface-hover);
|
|
243
|
+
}
|
|
244
|
+
.hp-sheet-close:active {
|
|
245
|
+
background: var(--surface-raised);
|
|
246
|
+
transition-duration: var(--dur-press);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/* A flex column, so a list inside can take the rest of the height
|
|
251
|
+
(flex: 1 1 auto; min-height: 0) and scroll on its own. */
|
|
252
|
+
.hp-sheet-body {
|
|
253
|
+
display: flex;
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
flex: 1 1 auto;
|
|
256
|
+
min-height: 0;
|
|
257
|
+
overflow-y: auto;
|
|
258
|
+
overscroll-behavior: contain;
|
|
259
|
+
}
|
|
260
|
+
.hp-sheet-footer {
|
|
261
|
+
flex: none;
|
|
262
|
+
}
|
|
263
|
+
</style>
|
|
@@ -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
|
+
}
|