@mystravil/hardpoint 0.0.0-stage → 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.
- package/CHANGELOG.md +38 -0
- package/LICENSE +25 -0
- package/README.md +64 -3
- package/dist/base.css +625 -0
- package/dist/components/AppShell.svelte +698 -0
- package/dist/components/AppShell.svelte.d.ts +77 -0
- package/dist/components/Button.svelte +174 -0
- package/dist/components/Button.svelte.d.ts +20 -0
- package/dist/components/Checkbox.svelte +235 -0
- package/dist/components/Checkbox.svelte.d.ts +23 -0
- package/dist/components/CheckboxGroup.svelte +207 -0
- package/dist/components/CheckboxGroup.svelte.d.ts +44 -0
- package/dist/components/Combobox.svelte +901 -0
- package/dist/components/Combobox.svelte.d.ts +72 -0
- package/dist/components/Dialog.svelte +359 -0
- package/dist/components/Dialog.svelte.d.ts +43 -0
- package/dist/components/Disclosure.svelte +201 -0
- package/dist/components/Disclosure.svelte.d.ts +27 -0
- package/dist/components/EmptyState.svelte +88 -0
- package/dist/components/EmptyState.svelte.d.ts +22 -0
- package/dist/components/ErrorState.svelte +135 -0
- package/dist/components/ErrorState.svelte.d.ts +26 -0
- package/dist/components/ImageViewer.svelte +764 -0
- package/dist/components/ImageViewer.svelte.d.ts +55 -0
- package/dist/components/KeyValueList.svelte +114 -0
- package/dist/components/KeyValueList.svelte.d.ts +43 -0
- package/dist/components/ListDetail.svelte +218 -0
- package/dist/components/ListDetail.svelte.d.ts +37 -0
- package/dist/components/LoadRegion.svelte +219 -0
- package/dist/components/LoadRegion.svelte.d.ts +27 -0
- package/dist/components/MediaFrame.svelte +110 -0
- package/dist/components/MediaFrame.svelte.d.ts +20 -0
- package/dist/components/MediaImage.svelte +256 -0
- package/dist/components/MediaImage.svelte.d.ts +16 -0
- package/dist/components/Menu.svelte +258 -0
- package/dist/components/Menu.svelte.d.ts +36 -0
- package/dist/components/MenuCheckboxItem.svelte +119 -0
- package/dist/components/MenuCheckboxItem.svelte.d.ts +15 -0
- package/dist/components/MenuGroup.svelte +50 -0
- package/dist/components/MenuGroup.svelte.d.ts +15 -0
- package/dist/components/MenuItem.svelte +94 -0
- package/dist/components/MenuItem.svelte.d.ts +19 -0
- package/dist/components/MenuRadioGroup.svelte +62 -0
- package/dist/components/MenuRadioGroup.svelte.d.ts +20 -0
- package/dist/components/MenuRadioItem.svelte +110 -0
- package/dist/components/MenuRadioItem.svelte.d.ts +17 -0
- package/dist/components/MenuSeparator.svelte +28 -0
- package/dist/components/MenuSeparator.svelte.d.ts +6 -0
- package/dist/components/Meter.svelte +283 -0
- package/dist/components/Meter.svelte.d.ts +66 -0
- package/dist/components/NativeSelect.svelte +135 -0
- package/dist/components/NativeSelect.svelte.d.ts +52 -0
- package/dist/components/Notice.svelte +262 -0
- package/dist/components/Notice.svelte.d.ts +30 -0
- package/dist/components/Pagination.svelte +132 -0
- package/dist/components/Pagination.svelte.d.ts +18 -0
- package/dist/components/Popover.svelte +222 -0
- package/dist/components/Popover.svelte.d.ts +35 -0
- package/dist/components/Progress.svelte +157 -0
- package/dist/components/Progress.svelte.d.ts +34 -0
- package/dist/components/RadioGroup.svelte +440 -0
- package/dist/components/RadioGroup.svelte.d.ts +50 -0
- package/dist/components/Select.svelte +471 -0
- package/dist/components/Select.svelte.d.ts +48 -0
- package/dist/components/Skeleton.svelte +149 -0
- package/dist/components/Skeleton.svelte.d.ts +24 -0
- package/dist/components/Spinner.svelte +118 -0
- package/dist/components/Spinner.svelte.d.ts +13 -0
- package/dist/components/Stat.svelte +253 -0
- package/dist/components/Stat.svelte.d.ts +46 -0
- package/dist/components/Steps.svelte +189 -0
- package/dist/components/Steps.svelte.d.ts +31 -0
- package/dist/components/Switch.svelte +222 -0
- package/dist/components/Switch.svelte.d.ts +18 -0
- package/dist/components/Table.svelte +504 -0
- package/dist/components/Table.svelte.d.ts +92 -0
- package/dist/components/Tabs.svelte +246 -0
- package/dist/components/Tabs.svelte.d.ts +44 -0
- package/dist/components/Tag.svelte +40 -0
- package/dist/components/Tag.svelte.d.ts +15 -0
- package/dist/components/TextArea.svelte +272 -0
- package/dist/components/TextArea.svelte.d.ts +28 -0
- package/dist/components/TextField.svelte +256 -0
- package/dist/components/TextField.svelte.d.ts +32 -0
- package/dist/components/Timeline.svelte +156 -0
- package/dist/components/Timeline.svelte.d.ts +30 -0
- package/dist/components/Toaster.svelte +411 -0
- package/dist/components/Toaster.svelte.d.ts +11 -0
- package/dist/components/Tooltip.svelte +200 -0
- package/dist/components/Tooltip.svelte.d.ts +38 -0
- package/dist/components/TooltipGroup.svelte +46 -0
- package/dist/components/TooltipGroup.svelte.d.ts +29 -0
- package/dist/components/internal/Scrim.svelte +52 -0
- package/dist/components/internal/Scrim.svelte.d.ts +15 -0
- package/dist/components/internal/Sheet.svelte +263 -0
- package/dist/components/internal/Sheet.svelte.d.ts +36 -0
- package/dist/components/internal/fade-in.d.ts +10 -0
- package/dist/components/internal/fade-in.js +20 -0
- package/dist/components/internal/groups.d.ts +14 -0
- package/dist/components/internal/groups.js +23 -0
- package/dist/components/internal/resolve.d.ts +8 -0
- package/dist/components/internal/resolve.js +247 -0
- package/dist/components/internal/swipe-dismiss.d.ts +47 -0
- package/dist/components/internal/swipe-dismiss.js +220 -0
- package/dist/components/internal/visual-viewport.svelte.d.ts +35 -0
- package/dist/components/internal/visual-viewport.svelte.js +79 -0
- package/dist/components/toast.svelte.d.ts +57 -0
- package/dist/components/toast.svelte.js +84 -0
- package/dist/fonts/OFL-geist-pixel.txt +92 -0
- package/dist/fonts/OFL.txt +93 -0
- package/dist/fonts/geist-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-mono-latin-variable.woff2 +0 -0
- package/dist/fonts/geist-pixel-square.woff2 +0 -0
- package/dist/fonts.css +71 -0
- package/dist/index.d.ts +45 -0
- package/dist/index.js +45 -0
- package/dist/motion/Motion.svelte +15 -0
- package/dist/motion/Motion.svelte.d.ts +9 -0
- package/dist/motion/context.d.ts +18 -0
- package/dist/motion/context.js +26 -0
- package/dist/motion/index.d.ts +3 -0
- package/dist/motion/index.js +3 -0
- package/dist/motion/transitions.d.ts +31 -0
- package/dist/motion/transitions.js +112 -0
- package/dist/tokens.css +283 -0
- package/package.json +76 -6
|
@@ -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;
|
|
@@ -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, 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. */
|
|
159
|
+
.hp-popover {
|
|
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, 360px), 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;
|