@dorsk/tsumikit 0.23.0 → 0.25.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/README.md +174 -6
- package/dist/components/atoms/Button.svelte +1 -1
- package/dist/components/atoms/Card.svelte +65 -2
- package/dist/components/atoms/Card.svelte.d.ts +6 -0
- package/dist/components/atoms/Scrim.svelte +81 -0
- package/dist/components/atoms/Scrim.svelte.d.ts +14 -0
- package/dist/components/layouts/ResizablePanel.svelte +238 -120
- package/dist/components/layouts/ResizablePanel.svelte.d.ts +24 -8
- package/dist/components/layouts/resizable-panel-frame.d.ts +70 -0
- package/dist/components/layouts/resizable-panel-frame.js +161 -0
- package/dist/components/molecules/ConfirmModal.svelte +102 -0
- package/dist/components/molecules/ConfirmModal.svelte.d.ts +16 -0
- package/dist/components/molecules/KeyValue.svelte +117 -0
- package/dist/components/molecules/KeyValue.svelte.d.ts +20 -0
- package/dist/components/molecules/LoadMore.svelte +78 -0
- package/dist/components/molecules/LoadMore.svelte.d.ts +15 -0
- package/dist/components/molecules/Modal.svelte +66 -10
- package/dist/components/molecules/Modal.svelte.d.ts +12 -2
- package/dist/components/molecules/Pagination.svelte +209 -0
- package/dist/components/molecules/Pagination.svelte.d.ts +22 -0
- package/dist/components/molecules/SectionHeader.svelte +233 -0
- package/dist/components/molecules/SectionHeader.svelte.d.ts +29 -0
- package/dist/components/molecules/ThemePicker.svelte +11 -13
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/stores/theme.svelte.d.ts +21 -5
- package/dist/stores/theme.svelte.js +48 -22
- package/dist/styles/app.css +7 -297
- package/dist/styles/reset.css +97 -0
- package/dist/styles/syntax.css +94 -0
- package/dist/styles/themes.css +729 -0
- package/dist/styles/tokens.css +207 -0
- package/dist/styles/utilities.css +111 -0
- package/dist/styles/variables.css +5 -926
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -57,15 +57,91 @@ import { Button, Field, Input, Modal, ThemePicker } from '@dorsk/tsumikit';
|
|
|
57
57
|
|
|
58
58
|
## Theming
|
|
59
59
|
|
|
60
|
-
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
`[data-theme="id"]` block in `variables.css`. Nothing else changes.
|
|
60
|
+
- 23 themes ship (light, highcontrast, gruvboxlight, solarizedlight,
|
|
61
|
+
everforestlight, rosepinedawn, latte, nordlight, tokyoday, kanagawalotus,
|
|
62
|
+
sepia, dark, **colorblind** — Okabe-Ito —, mocha, dracula, nord, tokyonight,
|
|
63
|
+
gruvbox, solarized, rosepine, onedark, everforest, monokai, amoled).
|
|
65
64
|
- `<ThemePicker />` and `<FontScalePicker />` wire the stores to the UI. Theme
|
|
66
65
|
is persisted to `localStorage` and applied with no flash (head snippet in
|
|
67
66
|
`app.html`) and updates the mobile `<meta name="theme-color">`.
|
|
68
67
|
|
|
68
|
+
### Stylesheets
|
|
69
|
+
|
|
70
|
+
`@dorsk/tsumikit/styles/app.css` is a shell over five files you can import
|
|
71
|
+
individually, in this order:
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
@import '@dorsk/tsumikit/styles/tokens.css';
|
|
75
|
+
@import '@dorsk/tsumikit/styles/themes.css'; /* optional */
|
|
76
|
+
@import '@dorsk/tsumikit/styles/reset.css';
|
|
77
|
+
@import '@dorsk/tsumikit/styles/utilities.css';
|
|
78
|
+
@import '@dorsk/tsumikit/styles/syntax.css';
|
|
79
|
+
@import './brand.css';
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| export | contents |
|
|
83
|
+
| --- | --- |
|
|
84
|
+
| `styles/tokens.css` | `:root` only — every token the kit reads (`--c-*` palette, `--bg`/`--text`/`--accent` aliases, type scale, spacing, radii, shadows, fonts, `--control-height`, …). Theme-less. |
|
|
85
|
+
| `styles/themes.css` | the built-in `[data-theme="id"]` blocks, one per `THEMES` entry. |
|
|
86
|
+
| `styles/reset.css` | reset + element defaults. |
|
|
87
|
+
| `styles/utilities.css` | `.container`, `.stack`, `.row`, `.sr-only`, icon sizing, … |
|
|
88
|
+
| `styles/syntax.css` | highlight.js / Prism class → `--syn-*` mapping. |
|
|
89
|
+
|
|
90
|
+
`styles/variables.css` = `tokens.css` + `themes.css` (kept for compatibility).
|
|
91
|
+
Nothing is wrapped in `@layer`: kit rules are unlayered so component-scoped
|
|
92
|
+
styles cascade exactly as before, and a consumer stylesheet imported **after**
|
|
93
|
+
the kit wins on source order at equal specificity. Do not put brand overrides
|
|
94
|
+
inside a `@layer` of your own — layered rules lose to the kit's unlayered ones.
|
|
95
|
+
|
|
96
|
+
### Your own theme, without vendoring
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
// +layout.ts (or any module that runs before first render)
|
|
100
|
+
import { theme } from '@dorsk/tsumikit';
|
|
101
|
+
theme.register({ id: 'kusaritoi', label: 'Kusaritoi', icon: '鎖', themeColor: '#2a2a2a', mode: 'dark' });
|
|
102
|
+
theme.setDefault('kusaritoi');
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```css
|
|
106
|
+
/* brand.css — imported after the kit stylesheet(s) */
|
|
107
|
+
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono&display=swap');
|
|
108
|
+
:root { --font-mono: 'JetBrains Mono', ui-monospace, monospace; }
|
|
109
|
+
|
|
110
|
+
[data-theme='kusaritoi'] {
|
|
111
|
+
color-scheme: dark;
|
|
112
|
+
--c-bg: #2a2a2a; --c-bg-elev: #323232; --c-bg-elev-2: #3a3a3a;
|
|
113
|
+
--c-surface: #323232; --c-border: #484848; --c-border-strong: #585858;
|
|
114
|
+
--c-text: #e8e8e8; --c-text-muted: #b0b0b0; --c-text-faint: #808080;
|
|
115
|
+
--c-accent: #5ac8c8; --c-accent-ink: #0d1f1f; --c-accent-dim: #3a8a8a;
|
|
116
|
+
--c-blue: #5a9fd4; --c-amber: #d9a543; --c-red: #e06060;
|
|
117
|
+
--c-green: #6abf69; --c-violet: #b48ef0; --c-gold: #d9a543;
|
|
118
|
+
--c-teal: #5ac8c8;
|
|
119
|
+
--shadow-sm: 0 1px 2px rgba(0, 0, 0, 0.4);
|
|
120
|
+
--shadow-md: 0 6px 20px rgba(0, 0, 0, 0.5);
|
|
121
|
+
--shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.6);
|
|
122
|
+
--mach-bg-sl: 45% 24%; --mach-fg-sl: 70% 82%; --mach-border-sl: 45% 40%;
|
|
123
|
+
}
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
Contract for a `[data-theme='x']` block: all nineteen `--c-*` palette tokens,
|
|
127
|
+
`--shadow-sm|md|lg`, `--mach-bg-sl|fg-sl|border-sl`, and `color-scheme: light`
|
|
128
|
+
for a light theme (`:root` defaults to dark, so dark themes may omit it).
|
|
129
|
+
`--md-code-bg` is optional (derives from `--c-blue`). Everything else (`--bg`,
|
|
130
|
+
`--text`, `--syn-*`, …) is an alias in `tokens.css` and follows automatically.
|
|
131
|
+
|
|
132
|
+
- `theme.register(def | def[])` appends to `theme.all` (built-ins first); an
|
|
133
|
+
entry with a built-in `id` replaces that built-in. `icon` and `themeColor` are
|
|
134
|
+
optional. `ThemePicker` reads `theme.all`, so registered themes appear at once.
|
|
135
|
+
- `theme.setDefault(id)` is the theme used when `localStorage` holds nothing
|
|
136
|
+
valid — set the same id as `data-theme` in your `app.html` so the no-flash
|
|
137
|
+
snippet and the store agree. A persisted id that only becomes valid after
|
|
138
|
+
`register()` is picked up as soon as it is registered.
|
|
139
|
+
- Only the palette or only the fonts? Import `tokens.css` + your `brand.css`
|
|
140
|
+
and skip `themes.css`; or import the full `app.css` and override `:root`
|
|
141
|
+
tokens after it. Never copy the kit files.
|
|
142
|
+
- Adding a built-in theme to the kit = one entry in `THEMES`
|
|
143
|
+
(`stores/theme.svelte.ts`) + one block in `styles/themes.css`.
|
|
144
|
+
|
|
69
145
|
## Components
|
|
70
146
|
|
|
71
147
|
**Atoms:** Text, Heading, Button, Input, Textarea, Select, Switch, Checkbox,
|
|
@@ -78,7 +154,8 @@ any custom SVG).
|
|
|
78
154
|
|
|
79
155
|
**Molecules:** Field, IconButton, SelectButton, Toggle, OptionButton, Modal,
|
|
80
156
|
Popover, Menu, Tabs, RadioGroup, Tooltip, Accordion, CopyButton, FileButton,
|
|
81
|
-
Dropzone, CodeBlock, Callout, EmptyState,
|
|
157
|
+
Dropzone, CodeBlock, Callout, EmptyState, ConfirmModal, Pagination, Toaster,
|
|
158
|
+
ThemePicker, FontScalePicker, SectionHeader, KeyValue, LoadMore.
|
|
82
159
|
|
|
83
160
|
**Organisms:** DataTable (generic `<T>`, typed columns + cell snippets;
|
|
84
161
|
`layout="fixed"` makes column widths authoritative, `Column.truncate` /
|
|
@@ -113,6 +190,32 @@ edge-to-edge sections — inside AppShell's main column use `size="none"` instea
|
|
|
113
190
|
Children can bleed to the column edge with
|
|
114
191
|
`margin-inline: calc(-1 * var(--container-gutter))`.
|
|
115
192
|
|
|
193
|
+
### ResizablePanel
|
|
194
|
+
|
|
195
|
+
Inline by default: `panel` shares the row with `children` on its `side`, drag the
|
|
196
|
+
edge or use the arrow keys/Home/End on the separator, collapse it with the edge
|
|
197
|
+
chevron; `width`/`minWidth`/`maxWidth` (px numbers or CSS lengths such as
|
|
198
|
+
`'12rem'`) and `widthKey` persist the width. `mode="overlay"` turns it into a
|
|
199
|
+
fixed non-modal drawer (`role="dialog"`) on the viewport edge: `bind:open`,
|
|
200
|
+
`onclose` fires on Escape, scrim click or the edge control, `scrim={false}` drops
|
|
201
|
+
the dim backdrop, `fullWidthBelow="960px"` makes the drawer span the viewport (no
|
|
202
|
+
handle, no scrim) on small screens, and `clampToViewport` (default true) keeps a
|
|
203
|
+
stored width inside a shrunken window. `children` is optional in overlay mode.
|
|
204
|
+
|
|
205
|
+
```svelte
|
|
206
|
+
<ResizablePanel mode="overlay" side="right" label="Conversation" bind:open
|
|
207
|
+
width={720} minWidth={360} maxWidth="90vw" widthKey="conv-w" fullWidthBelow="960px">
|
|
208
|
+
{#snippet panel()}…{/snippet}
|
|
209
|
+
</ResizablePanel>
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
`Scrim` (`onclose`, `hideBelow`, `z`, `label`) is the drawer's backdrop atom,
|
|
213
|
+
exported for custom overlays: click or Escape (document-level) calls `onclose`.
|
|
214
|
+
`resizeHandle` is the shared drag action behind every grip: `use:resizeHandle={{
|
|
215
|
+
side, min, max, step, onwidth, oncommit, onreset, onactive, measure }}` gives
|
|
216
|
+
any element pointer-capture + rAF-coalesced dragging, arrow/Home/End keys and
|
|
217
|
+
double-click reset; the width defaults to the element's parent box.
|
|
218
|
+
|
|
116
219
|
### Stacked distribution + legend
|
|
117
220
|
|
|
118
221
|
`SegmentedProgress mode="stacked"` turns the bar into one shared track whose slice
|
|
@@ -153,6 +256,71 @@ is a live region — `role="status"`, or `role="alert"` when `tone="danger"`.
|
|
|
153
256
|
</Callout>
|
|
154
257
|
```
|
|
155
258
|
|
|
259
|
+
### Modal, ConfirmModal & Pagination
|
|
260
|
+
|
|
261
|
+
`Modal` opens on mount by default; pass `bind:open` instead to keep it mounted
|
|
262
|
+
and drive `showModal()`/`close()` from state (closing writes `open = false`).
|
|
263
|
+
`tone="danger" | "warn" | "info"` adds a title glyph and a 3px top border;
|
|
264
|
+
`busy` makes the body inert, shows a spinner by the title and disables Escape,
|
|
265
|
+
backdrop and the close button. The `footer` snippet is a right-aligned flex row
|
|
266
|
+
(`justify-content: flex-end; gap: var(--sp-2)`).
|
|
267
|
+
|
|
268
|
+
`ConfirmModal` wraps it as a yes/no dialog: `title`, `message` (or `children`),
|
|
269
|
+
`confirmLabel`/`cancelLabel`, `tone="primary" | "danger" | "warn"`, `busy`,
|
|
270
|
+
`onconfirm`, `oncancel`. An async `onconfirm` keeps the dialog busy until it
|
|
271
|
+
settles, closes only on success and shows a rejection's message (`role="alert"`)
|
|
272
|
+
under the body. `Button` accepts `tone="danger"` as an alias of
|
|
273
|
+
`variant="danger"` so the tone axis works alone.
|
|
274
|
+
|
|
275
|
+
```svelte
|
|
276
|
+
<ConfirmModal bind:open title="Delete library?" tone="danger" confirmLabel="Delete"
|
|
277
|
+
onconfirm={() => api.deleteLibrary(id)} />
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
`Pagination` renders `<nav aria-label>` with prev/next IconButtons, numbered
|
|
281
|
+
pages (`aria-current="page"`), ellipses and an optional `showRange` readout.
|
|
282
|
+
Page mode: `bind:page` + `pageCount`. Offset mode: `bind:offset` + `limit` +
|
|
283
|
+
`total` (page and count are derived, `offset` is written back). `onchange(page)`,
|
|
284
|
+
`siblings` (1), `showEdges` (true), `size="sm" | "md"`, `label`. Under 24rem of
|
|
285
|
+
container width it collapses to prev / "3 / 12" / next.
|
|
286
|
+
|
|
287
|
+
```svelte
|
|
288
|
+
<Pagination bind:offset limit={20} total={412} showRange onchange={load} />
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
### SectionHeader, Card header/footer, KeyValue, LoadMore
|
|
292
|
+
|
|
293
|
+
`SectionHeader` is the one "title + meta + right-aligned actions" row:
|
|
294
|
+
`title` (or `label`), `level` 1–4 (default 2), `size`, `subtitle`, `icon`,
|
|
295
|
+
`count` (faint tabular figure after the title), `tone`, `hue` (0–360 swatch
|
|
296
|
+
chip), `uppercase` (eyebrow group label), `divider`, `sticky` (pins at
|
|
297
|
+
`--sticky-offset` / `--header-h` and publishes `--section-header-h` on its
|
|
298
|
+
parent), `collapsible` + `bind:open` (title becomes a disclosure button with
|
|
299
|
+
`aria-expanded`; `children` render beneath while open), `actions` snippet.
|
|
300
|
+
|
|
301
|
+
`Card` gains `header` / `footer` snippets rendered outside the padded body
|
|
302
|
+
behind a divider, `title` / `subtitle` / `actions` sugar that renders a
|
|
303
|
+
`SectionHeader` in the header slot, and `gap` to stack children as a flex
|
|
304
|
+
column. A Card without any of these renders exactly as before.
|
|
305
|
+
|
|
306
|
+
`KeyValue` renders `rows: { label, value: string | number | Snippet, mono?,
|
|
307
|
+
tone?, hint? }[]` as a semantic `<dl>` grid; `columns` 1 | 2, `dense`, `align`
|
|
308
|
+
start | end. `LoadMore` is the tri-state list footer: `state` idle | loading |
|
|
309
|
+
error | done, `onload`, `label`, `loadingLabel`, `errorLabel`, `retryLabel`,
|
|
310
|
+
`doneLabel`, `pill` for the compact "load older" chip.
|
|
311
|
+
|
|
312
|
+
```svelte
|
|
313
|
+
<Card title="Recent sessions" subtitle="last 24h">
|
|
314
|
+
{#snippet actions()}<Link href="/sessions">View all</Link>{/snippet}
|
|
315
|
+
<KeyValue rows={[{ label: 'Running', value: 3, tone: 'ok' }, { label: 'Host', value: 'sakura', mono: true }]} />
|
|
316
|
+
{#snippet footer()}<LoadMore state={more} onload={loadMore} />{/snippet}
|
|
317
|
+
</Card>
|
|
318
|
+
|
|
319
|
+
<SectionHeader label="Blocked" count={4} uppercase hue={12} collapsible bind:open>
|
|
320
|
+
…group rows…
|
|
321
|
+
</SectionHeader>
|
|
322
|
+
```
|
|
323
|
+
|
|
156
324
|
## Container queries
|
|
157
325
|
|
|
158
326
|
AppShell's `main` and `sidebar` are query containers (`container-name: main` /
|
|
@@ -112,7 +112,7 @@
|
|
|
112
112
|
class:btn-grow={grow}
|
|
113
113
|
class:btn-primary={variant === 'primary'}
|
|
114
114
|
class:btn-ghost={variant === 'ghost'}
|
|
115
|
-
class:btn-danger={variant === 'danger'}
|
|
115
|
+
class:btn-danger={variant === 'danger' || (tone === 'danger' && variant === 'default')}
|
|
116
116
|
class:btn-sm={size === 'sm'}
|
|
117
117
|
class:btn-lg={size === 'lg'}
|
|
118
118
|
class:btn-control={control}
|
|
@@ -13,7 +13,12 @@
|
|
|
13
13
|
// keeps them on the plain border colour. `stackY`/`stackX` set the per-layer
|
|
14
14
|
// vertical / horizontal offset in px (vertical spacing stays even across the
|
|
15
15
|
// 3 borders); horizontal defaults to a tiny 2px peek.
|
|
16
|
+
//
|
|
17
|
+
// `header`/`footer` snippets (or the `title`/`subtitle`/`actions` sugar,
|
|
18
|
+
// which renders a SectionHeader) sit outside the padded body behind a
|
|
19
|
+
// divider; `gap` stacks children as a flex column without a wrapper.
|
|
16
20
|
import type { Snippet } from 'svelte';
|
|
21
|
+
import SectionHeader from '../molecules/SectionHeader.svelte';
|
|
17
22
|
|
|
18
23
|
type Tone = 'neutral' | 'ok' | 'warn' | 'danger' | 'info';
|
|
19
24
|
|
|
@@ -27,8 +32,14 @@
|
|
|
27
32
|
stackTone = 'neutral',
|
|
28
33
|
stackY = 8,
|
|
29
34
|
stackX = 2,
|
|
35
|
+
title,
|
|
36
|
+
subtitle,
|
|
37
|
+
gap,
|
|
30
38
|
class: klass = '',
|
|
31
39
|
style = '',
|
|
40
|
+
header,
|
|
41
|
+
footer,
|
|
42
|
+
actions,
|
|
32
43
|
children,
|
|
33
44
|
...rest
|
|
34
45
|
}: {
|
|
@@ -41,8 +52,14 @@
|
|
|
41
52
|
stackTone?: Tone;
|
|
42
53
|
stackY?: number;
|
|
43
54
|
stackX?: number;
|
|
55
|
+
title?: string;
|
|
56
|
+
subtitle?: string;
|
|
57
|
+
gap?: string;
|
|
44
58
|
class?: string;
|
|
45
59
|
style?: string;
|
|
60
|
+
header?: Snippet;
|
|
61
|
+
footer?: Snippet;
|
|
62
|
+
actions?: Snippet;
|
|
46
63
|
children?: Snippet;
|
|
47
64
|
[key: string]: unknown;
|
|
48
65
|
} = $props();
|
|
@@ -50,6 +67,7 @@
|
|
|
50
67
|
let stackStyle = $derived(
|
|
51
68
|
stacked ? `--stack-y:${stackY}px;--stack-x:${stackX}px;` : ''
|
|
52
69
|
);
|
|
70
|
+
const framed = $derived(!!(header || footer || title));
|
|
53
71
|
</script>
|
|
54
72
|
|
|
55
73
|
<svelte:element
|
|
@@ -71,18 +89,55 @@
|
|
|
71
89
|
class:stack-warn={stacked && stackTone === 'warn'}
|
|
72
90
|
class:stack-danger={stacked && stackTone === 'danger'}
|
|
73
91
|
class:stack-info={stacked && stackTone === 'info'}
|
|
92
|
+
class:card-framed={framed}
|
|
93
|
+
class:card-gap={!framed && gap !== undefined}
|
|
94
|
+
style:--card-gap={gap}
|
|
74
95
|
style={`${stackStyle}${style}`}
|
|
75
96
|
{...rest}
|
|
76
97
|
>
|
|
77
|
-
{
|
|
98
|
+
{#if framed}
|
|
99
|
+
{#if title}
|
|
100
|
+
<div class="card-head">
|
|
101
|
+
<SectionHeader {title} {subtitle} {actions} level={3} size="md" />
|
|
102
|
+
</div>
|
|
103
|
+
{:else if header}
|
|
104
|
+
<div class="card-head">{@render header()}</div>
|
|
105
|
+
{/if}
|
|
106
|
+
<div class="card-body" class:card-gap={gap !== undefined}>{@render children?.()}</div>
|
|
107
|
+
{#if footer}
|
|
108
|
+
<div class="card-foot">{@render footer()}</div>
|
|
109
|
+
{/if}
|
|
110
|
+
{:else}
|
|
111
|
+
{@render children?.()}
|
|
112
|
+
{/if}
|
|
78
113
|
</svelte:element>
|
|
79
114
|
|
|
80
115
|
<style>
|
|
81
116
|
.card {
|
|
117
|
+
--card-pad: var(--sp-4);
|
|
82
118
|
background: var(--bg-elevated);
|
|
83
119
|
border: 1px solid var(--border);
|
|
84
120
|
border-radius: var(--r-lg);
|
|
85
|
-
padding: var(--
|
|
121
|
+
padding: var(--card-pad);
|
|
122
|
+
}
|
|
123
|
+
.card-gap {
|
|
124
|
+
display: flex;
|
|
125
|
+
flex-direction: column;
|
|
126
|
+
gap: var(--card-gap);
|
|
127
|
+
}
|
|
128
|
+
.card-framed {
|
|
129
|
+
padding: 0;
|
|
130
|
+
}
|
|
131
|
+
.card-head,
|
|
132
|
+
.card-body,
|
|
133
|
+
.card-foot {
|
|
134
|
+
padding: var(--card-pad);
|
|
135
|
+
}
|
|
136
|
+
.card-head {
|
|
137
|
+
border-bottom: 1px solid var(--border);
|
|
138
|
+
}
|
|
139
|
+
.card-foot {
|
|
140
|
+
border-top: 1px solid var(--border);
|
|
86
141
|
}
|
|
87
142
|
/* Theme-aware surface shade, selected by prop so it adapts across themes
|
|
88
143
|
instead of being overridden with app-level :global hacks. `base` is the
|
|
@@ -94,14 +149,22 @@
|
|
|
94
149
|
background: var(--bg-elevated-2);
|
|
95
150
|
}
|
|
96
151
|
.pad-none {
|
|
152
|
+
--card-pad: 0;
|
|
97
153
|
padding: 0;
|
|
98
154
|
}
|
|
99
155
|
.pad-sm {
|
|
156
|
+
--card-pad: var(--sp-2);
|
|
100
157
|
padding: var(--sp-2);
|
|
101
158
|
}
|
|
102
159
|
.pad-lg {
|
|
160
|
+
--card-pad: var(--sp-6);
|
|
103
161
|
padding: var(--sp-6);
|
|
104
162
|
}
|
|
163
|
+
.card-framed.pad-none,
|
|
164
|
+
.card-framed.pad-sm,
|
|
165
|
+
.card-framed.pad-lg {
|
|
166
|
+
padding: 0;
|
|
167
|
+
}
|
|
105
168
|
.card-tap {
|
|
106
169
|
cursor: pointer;
|
|
107
170
|
transition:
|
|
@@ -10,8 +10,14 @@ type $$ComponentProps = {
|
|
|
10
10
|
stackTone?: Tone;
|
|
11
11
|
stackY?: number;
|
|
12
12
|
stackX?: number;
|
|
13
|
+
title?: string;
|
|
14
|
+
subtitle?: string;
|
|
15
|
+
gap?: string;
|
|
13
16
|
class?: string;
|
|
14
17
|
style?: string;
|
|
18
|
+
header?: Snippet;
|
|
19
|
+
footer?: Snippet;
|
|
20
|
+
actions?: Snippet;
|
|
15
21
|
children?: Snippet;
|
|
16
22
|
[key: string]: unknown;
|
|
17
23
|
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { browser } from '../../env';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
onclose,
|
|
6
|
+
hideBelow,
|
|
7
|
+
z = 'var(--z-drawer)',
|
|
8
|
+
label = 'Close'
|
|
9
|
+
}: {
|
|
10
|
+
/** Called on click anywhere on the scrim and on Escape (document-level). */
|
|
11
|
+
onclose?: () => void;
|
|
12
|
+
/** Viewport width (CSS length) under which the scrim is not rendered,
|
|
13
|
+
* for overlays that go full-bleed on small screens. */
|
|
14
|
+
hideBelow?: string;
|
|
15
|
+
/** z-index; defaults to the drawer layer. */
|
|
16
|
+
z?: string | number;
|
|
17
|
+
/** Accessible name of the click target. */
|
|
18
|
+
label?: string;
|
|
19
|
+
} = $props();
|
|
20
|
+
|
|
21
|
+
let hidden = $state(false);
|
|
22
|
+
|
|
23
|
+
$effect(() => {
|
|
24
|
+
if (!browser || !hideBelow) {
|
|
25
|
+
hidden = false;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
const query = matchMedia(`(max-width: ${hideBelow})`);
|
|
29
|
+
const sync = () => {
|
|
30
|
+
hidden = query.matches;
|
|
31
|
+
};
|
|
32
|
+
sync();
|
|
33
|
+
query.addEventListener('change', sync);
|
|
34
|
+
return () => query.removeEventListener('change', sync);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
$effect(() => {
|
|
38
|
+
if (!browser) return;
|
|
39
|
+
const onKeydown = (event: KeyboardEvent) => {
|
|
40
|
+
if (event.key !== 'Escape' || event.defaultPrevented) return;
|
|
41
|
+
event.preventDefault();
|
|
42
|
+
onclose?.();
|
|
43
|
+
};
|
|
44
|
+
document.addEventListener('keydown', onKeydown);
|
|
45
|
+
return () => document.removeEventListener('keydown', onKeydown);
|
|
46
|
+
});
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
{#if !hidden}
|
|
50
|
+
<button
|
|
51
|
+
type="button"
|
|
52
|
+
class="scrim"
|
|
53
|
+
style:z-index={z}
|
|
54
|
+
aria-label={label}
|
|
55
|
+
tabindex="-1"
|
|
56
|
+
onclick={() => onclose?.()}
|
|
57
|
+
data-tsu="Scrim"
|
|
58
|
+
></button>
|
|
59
|
+
{/if}
|
|
60
|
+
|
|
61
|
+
<style>
|
|
62
|
+
.scrim {
|
|
63
|
+
position: fixed;
|
|
64
|
+
inset: 0;
|
|
65
|
+
padding: 0;
|
|
66
|
+
border: 0;
|
|
67
|
+
background: color-mix(in srgb, var(--bg) 45%, transparent);
|
|
68
|
+
cursor: default;
|
|
69
|
+
animation: scrim-fade 0.18s var(--ease);
|
|
70
|
+
}
|
|
71
|
+
@keyframes scrim-fade {
|
|
72
|
+
from {
|
|
73
|
+
opacity: 0;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
@media (prefers-reduced-motion: reduce) {
|
|
77
|
+
.scrim {
|
|
78
|
+
animation: none;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
</style>
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type $$ComponentProps = {
|
|
2
|
+
/** Called on click anywhere on the scrim and on Escape (document-level). */
|
|
3
|
+
onclose?: () => void;
|
|
4
|
+
/** Viewport width (CSS length) under which the scrim is not rendered,
|
|
5
|
+
* for overlays that go full-bleed on small screens. */
|
|
6
|
+
hideBelow?: string;
|
|
7
|
+
/** z-index; defaults to the drawer layer. */
|
|
8
|
+
z?: string | number;
|
|
9
|
+
/** Accessible name of the click target. */
|
|
10
|
+
label?: string;
|
|
11
|
+
};
|
|
12
|
+
declare const Scrim: import("svelte").Component<$$ComponentProps, {}, "">;
|
|
13
|
+
type Scrim = ReturnType<typeof Scrim>;
|
|
14
|
+
export default Scrim;
|