@poodle64/ui 2026.10.17 → 2026.10.18
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 +94 -11
- package/dist/components/feedback/report-widget.svelte +4 -1
- package/dist/components/feedback/report-widget.svelte.d.ts +1 -0
- package/dist/components/ui/app-identity/app-identity.svelte +7 -2
- package/dist/components/ui/app-shell/texture.d.ts +5 -5
- package/dist/components/ui/app-shell/texture.js +3 -3
- package/dist/components/ui/breadcrumbs/breadcrumbs.svelte +70 -0
- package/dist/components/ui/breadcrumbs/breadcrumbs.svelte.d.ts +32 -0
- package/dist/components/ui/breadcrumbs/index.d.ts +2 -0
- package/dist/components/ui/breadcrumbs/index.js +2 -0
- package/dist/components/ui/content-skeleton/content-skeleton.svelte +94 -0
- package/dist/components/ui/content-skeleton/content-skeleton.svelte.d.ts +25 -0
- package/dist/components/ui/content-skeleton/index.d.ts +2 -0
- package/dist/components/ui/content-skeleton/index.js +2 -0
- package/dist/components/ui/document-reader/document-reader.svelte +336 -0
- package/dist/components/ui/document-reader/document-reader.svelte.d.ts +65 -0
- package/dist/components/ui/document-reader/index.d.ts +2 -0
- package/dist/components/ui/document-reader/index.js +2 -0
- package/dist/components/ui/page-container/index.d.ts +2 -0
- package/dist/components/ui/page-container/index.js +2 -0
- package/dist/components/ui/page-container/page-container.svelte +39 -0
- package/dist/components/ui/page-container/page-container.svelte.d.ts +24 -0
- package/dist/components/ui/page-header/page-header.svelte +12 -7
- package/dist/components/ui/page-header/page-header.svelte.d.ts +3 -2
- package/dist/components/ui/page-section/index.d.ts +2 -0
- package/dist/components/ui/page-section/index.js +2 -0
- package/dist/components/ui/page-section/page-section.svelte +92 -0
- package/dist/components/ui/page-section/page-section.svelte.d.ts +36 -0
- package/dist/components/ui/sheet/index.d.ts +13 -0
- package/dist/components/ui/sheet/index.js +15 -0
- package/dist/components/ui/sheet/sheet-body.svelte +21 -0
- package/dist/components/ui/sheet/sheet-body.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-close.svelte +11 -0
- package/dist/components/ui/sheet/sheet-close.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-content.svelte +70 -0
- package/dist/components/ui/sheet/sheet-content.svelte.d.ts +37 -0
- package/dist/components/ui/sheet/sheet-description.svelte +20 -0
- package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-footer.svelte +23 -0
- package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-header.svelte +21 -0
- package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte +20 -0
- package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-portal.svelte +7 -0
- package/dist/components/ui/sheet/sheet-portal.svelte.d.ts +3 -0
- package/dist/components/ui/sheet/sheet-title.svelte +21 -0
- package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte +11 -0
- package/dist/components/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
- package/dist/components/ui/sheet/sheet.svelte +7 -0
- package/dist/components/ui/sheet/sheet.svelte.d.ts +3 -0
- package/dist/components/ui/skeleton/skeleton.svelte +1 -1
- package/dist/styles.css +8 -0
- package/package.json +1 -1
- package/registry/component-map.json +43 -4
- package/registry/component-map.md +9 -4
package/README.md
CHANGED
|
@@ -47,22 +47,25 @@ src/lib/
|
|
|
47
47
|
dist/ generated by `pnpm build` (@sveltejs/package); never edit
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
**Primitives** (
|
|
50
|
+
**Primitives** (27 — battle-tested implementations pulled from whichever app had
|
|
51
51
|
them first, not an invented "ideal" list): `alert`, `alert-dialog`, `avatar`,
|
|
52
52
|
`badge`, `button`, `card`, `checkbox`, `command`, `data-table`, `dialog`,
|
|
53
53
|
`dropdown-menu`, `input`, `input-group`, `label`, `password-input`, `popover`,
|
|
54
|
-
`progress`, `select`, `separator`, `skeleton`, `sonner`, `switch`,
|
|
55
|
-
`tabs`, `textarea`, `tooltip`.
|
|
54
|
+
`progress`, `select`, `separator`, `sheet`, `skeleton`, `sonner`, `switch`,
|
|
55
|
+
`table`, `tabs`, `textarea`, `tooltip`.
|
|
56
56
|
|
|
57
57
|
`avatar` is root, image and fallback only; the group and badge variants in the
|
|
58
58
|
upstream registry had no consumer in any surveyed app. It is here because
|
|
59
59
|
`AppShell`'s `identity` slot needs one, and a consumer filling a slot the shell
|
|
60
60
|
itself defines should not have to keep a private copy of a primitive to do it.
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
62
|
+
`sheet` is shadcn's API (`Sheet.Root`, `Trigger`, `Content side=…`, `Header`,
|
|
63
|
+
`Title`, `Description`, `Footer`, `Close`) plus `Sheet.Body`, the part that
|
|
64
|
+
scrolls between a fixed header and footer. It is a panel over the page, for a
|
|
65
|
+
task beside what you are reading (comments on a chapter, a cited record on a
|
|
66
|
+
phone); it is never a second navigation column, which the shell has no slot
|
|
67
|
+
for. It stands on the kit's hairline depth, not a shadow, and inside an
|
|
68
|
+
element's `<Overlays>` it portals into the shadow root like the dialogue does.
|
|
66
69
|
|
|
67
70
|
**Deliberately not here: `chart` and `form`.** Two apps keep local copies of each,
|
|
68
71
|
and both stay local for reasons that are about coupling rather than effort.
|
|
@@ -89,7 +92,12 @@ rather than rebuilding it:
|
|
|
89
92
|
|
|
90
93
|
| Import | What it is |
|
|
91
94
|
| ----------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
92
|
-
| `page-header` | The only page-title pattern: optional `breadcrumbs` and `icon
|
|
95
|
+
| `page-header` | The only page-title pattern: optional `breadcrumbs` (the trail's items, or a snippet) and `icon`, eyebrow, an optional title, one clamped subtitle, an `info` tooltip, a `meta` row and an `actions` slot. Omit `title` for a header that is a breadcrumb bar. |
|
|
96
|
+
| `breadcrumbs` | The trail: `items` of `{ label, href? }`, a named landmark, chevrons, each step truncating rather than wrapping. PageHeader draws it from `breadcrumbs={[…]}`. See [The page](#the-page-container-sections-and-the-type-scale). |
|
|
97
|
+
| `page-container` | A route's page at the one rhythm: header, sections and panels 1.5rem apart, filling the content box. |
|
|
98
|
+
| `page-section` | A section of a page: `title` at `level` 2 or 3, an optional `description`, `count` and `actions`, and its content at one distance below. |
|
|
99
|
+
| `document-reader` | A document to read: PageHeader, the body (`html` the app sanitised, or `children`) in `.ds-prose` at the reading measure, and a contents list from its own headings. See [A document to read](#a-document-to-read). |
|
|
100
|
+
| `content-skeleton` | A region loading, shaped like what replaces it: `rows`, `tiles` or `prose`. A whole page waiting is LoadingState. |
|
|
93
101
|
| `panel` | The generic titled card, and the settings section: optional icon, a clamped one-line `subtitle` or a wrapping `description`, trailing `action`s, a body that can opt out of padding or scroll inside the card (`scroll`), an optional `footer` strip for Save/Cancel, and `tone="destructive"` for a danger zone. There is no `SettingsSection`; see [The settings destination](#the-settings-destination). |
|
|
94
102
|
| `list-toolbar` | The bar above a list, on the page ground: title, count, icon actions named by their tooltips, one labelled action. The Ledger and the RecordList draw theirs with it. |
|
|
95
103
|
| `record-list` | The ledger's look for a list that is not money. See [Lists that are not money](#lists-that-are-not-money). |
|
|
@@ -534,9 +542,13 @@ Two things worth knowing before you set it:
|
|
|
534
542
|
replaces. An app running `padded={false}` and its own page padding gets the cap
|
|
535
543
|
flush, which is usually what it wanted.
|
|
536
544
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
545
|
+
A route declares it in its load data and the app's frame passes it on: the
|
|
546
|
+
stamped frame renders `measure={page.data.measure ?? 'wide'}`, and a route group
|
|
547
|
+
returns `{ measure: 'page' }` from its `+layout.ts`, one route from its
|
|
548
|
+
`+page.ts`. Three apps had each written a table of path regexes mapping routes
|
|
549
|
+
to tiers in their frame (22 rows in one); route data puts the decision on the
|
|
550
|
+
route it belongs to, and SvelteKit merges a layout's answer into every page
|
|
551
|
+
under it. `padded` travels the same way.
|
|
540
552
|
|
|
541
553
|
#### A block inside the page: `.ds-measure`
|
|
542
554
|
|
|
@@ -562,6 +574,77 @@ paragraphs ~207px away from their own label. Both halves are gated in
|
|
|
562
574
|
on the page's own left edge, and it follows a retune of
|
|
563
575
|
`--ds-shell-measure-prose`.
|
|
564
576
|
|
|
577
|
+
### The page: container, sections and the type scale
|
|
578
|
+
|
|
579
|
+
```svelte
|
|
580
|
+
<PageContainer>
|
|
581
|
+
<PageHeader
|
|
582
|
+
title="Allowances"
|
|
583
|
+
breadcrumbs={[{ label: 'Library', href: '/library' }, { label: 'Pay and conditions' }]}
|
|
584
|
+
/>
|
|
585
|
+
<PageSection title="Recently changed" count={4} description="Clauses amended this quarter.">
|
|
586
|
+
{#snippet actions()}<Button variant="outline" size="sm">Compare</Button>{/snippet}
|
|
587
|
+
<RecordList … />
|
|
588
|
+
</PageSection>
|
|
589
|
+
</PageContainer>
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
A step with an `href` is a way back; the last step without one is where you are
|
|
593
|
+
(`aria-current="page"`). The trail is a `<nav aria-label="Breadcrumb">`, which
|
|
594
|
+
none of the three hand-written copies had.
|
|
595
|
+
|
|
596
|
+
The type scale, by role. An app sets none of these; a route sets no size of its
|
|
597
|
+
own and composes the component that owns the role:
|
|
598
|
+
|
|
599
|
+
| Role | Utility | Size | Face | Owned by |
|
|
600
|
+
| --- | --- | --- | --- | --- |
|
|
601
|
+
| Page title | `text-display` | 1.75rem | display, semibold | PageHeader |
|
|
602
|
+
| Section | `text-section` | 1.25rem | display, semibold | PageSection `level={2}` |
|
|
603
|
+
| Panel title, subsection | `text-body` | 0.9375rem | body, semibold | Panel, PageSection `level={3}` |
|
|
604
|
+
| Readout | `text-stat` | 1.0625rem | body | StatList |
|
|
605
|
+
| Reading text | `text-base` | 1rem | body | DocumentReader |
|
|
606
|
+
| Body | `text-sm` | 0.875rem | body | the default |
|
|
607
|
+
| Facts, captions | `text-xs` | 0.75rem | body, muted | PageHeader `meta` |
|
|
608
|
+
| Column head | `text-2xs`, uppercase, `tracking-eyebrow` | 0.6875rem | body, medium | tables, nav groups |
|
|
609
|
+
|
|
610
|
+
PageSection has no uppercase-kicker form. One app had five recipes for a
|
|
611
|
+
section heading, three of them small caps; small caps over content is a column
|
|
612
|
+
head, and as a heading it is the generated-UI tell `eyebrow` warns about.
|
|
613
|
+
|
|
614
|
+
### A document to read
|
|
615
|
+
|
|
616
|
+
```svelte
|
|
617
|
+
<DocumentReader
|
|
618
|
+
title={doc.title}
|
|
619
|
+
breadcrumbs={[{ label: 'Research notes', href: '/research' }, { label: doc.title }]}
|
|
620
|
+
html={sanitised}
|
|
621
|
+
{loading}
|
|
622
|
+
{error}
|
|
623
|
+
{onRetry}
|
|
624
|
+
>
|
|
625
|
+
{#snippet meta()}<span>12 min read</span>{/snippet}
|
|
626
|
+
{#snippet footer()}<a href={next.href}>Next: {next.title}</a>{/snippet}
|
|
627
|
+
</DocumentReader>
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The body is the app's HTML, already sanitised, for the reason `.ds-prose` gives;
|
|
631
|
+
strip the body's own leading `# Title`, since the header carries it. The
|
|
632
|
+
contents list is the body's `h2` and `h3`, shown once there are two; a heading
|
|
633
|
+
without an id gets one from its text. From a reader 64rem wide the list stands
|
|
634
|
+
beside the body and stays in view; narrower, it folds into an "On this page"
|
|
635
|
+
disclosure above it. That is a container query on the reader, so a route at
|
|
636
|
+
`measure="page"` or wider shows it beside the body on a laptop. A link scrolls
|
|
637
|
+
to its heading and moves focus there, without the URL fragment, which cannot
|
|
638
|
+
reach into a shadow root.
|
|
639
|
+
|
|
640
|
+
`aside` adds to that column, under the contents, and follows the body on a
|
|
641
|
+
narrower reader: a library document's catalogue facts belong there, and
|
|
642
|
+
DocumentDetail stays the record for a document with no body to read. The column
|
|
643
|
+
is held while the body loads, so a structured document does not move sideways
|
|
644
|
+
as it arrives; a body with fewer than two headings and no `aside` gives the room
|
|
645
|
+
back. With a `title`, `loading` keeps the header and draws the body as a
|
|
646
|
+
`prose` ContentSkeleton; without one it is LoadingState.
|
|
647
|
+
|
|
565
648
|
### The content texture
|
|
566
649
|
|
|
567
650
|
The house atmosphere — a faint dot-grid floor with a soft accent vignette in the
|
|
@@ -33,10 +33,13 @@
|
|
|
33
33
|
let {
|
|
34
34
|
/** Where the report POSTs. The app's own route, defaulting to the one every household app serves. */
|
|
35
35
|
endpoint = '/api/feedback',
|
|
36
|
+
/** Who reads a report, as "the <team> team". The app's own name, never an internal one. */
|
|
37
|
+
team = 'support',
|
|
36
38
|
/** Fired with the id the endpoint returned, once, on the success leg. */
|
|
37
39
|
onSubmitted
|
|
38
40
|
}: {
|
|
39
41
|
endpoint?: string;
|
|
42
|
+
team?: string;
|
|
40
43
|
onSubmitted?: (id: string) => void;
|
|
41
44
|
} = $props();
|
|
42
45
|
|
|
@@ -129,7 +132,7 @@
|
|
|
129
132
|
<AppDialog
|
|
130
133
|
bind:open
|
|
131
134
|
title="Report a problem"
|
|
132
|
-
subtitle="Describe what happened and the
|
|
135
|
+
subtitle="Describe what happened and the {team} team will take a look."
|
|
133
136
|
size="sm"
|
|
134
137
|
onOpenChange={(next) => !next && reset()}
|
|
135
138
|
>
|
|
@@ -143,6 +143,11 @@
|
|
|
143
143
|
if (onSwitchTheme) onSwitchTheme();
|
|
144
144
|
else toggleMode();
|
|
145
145
|
}
|
|
146
|
+
|
|
147
|
+
// Each name once: the identity provider repeats a name for every binding
|
|
148
|
+
// that grants it, and a repeated key throws in the list below, which took
|
|
149
|
+
// one app's whole account menu down with it.
|
|
150
|
+
const held = $derived([...new Set(entitlements)]);
|
|
146
151
|
</script>
|
|
147
152
|
|
|
148
153
|
<DropdownMenu.Root>
|
|
@@ -193,9 +198,9 @@
|
|
|
193
198
|
</span>
|
|
194
199
|
{/if}
|
|
195
200
|
</div>
|
|
196
|
-
{#if
|
|
201
|
+
{#if held.length}
|
|
197
202
|
<div class="flex flex-wrap gap-1 px-2 pb-2">
|
|
198
|
-
{#each
|
|
203
|
+
{#each held as e (e)}
|
|
199
204
|
<span class="bg-surface-2 text-muted-foreground font-mono rounded px-1.5 py-px text-[10px]">
|
|
200
205
|
{e}
|
|
201
206
|
</span>
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* it is a coin toss with a stylesheet.
|
|
13
13
|
*
|
|
14
14
|
* So it moves onto the shell, where there is exactly one place to say it and no
|
|
15
|
-
* per-route decision left to get wrong. `
|
|
16
|
-
*
|
|
15
|
+
* per-route decision left to get wrong. `grid` is the default: an opt-in house
|
|
16
|
+
* style measured who remembered it, not what the house looks like.
|
|
17
17
|
*
|
|
18
18
|
* It is one texture, not a menu. Both apps wanted the same picture, and the
|
|
19
19
|
* axes they actually differed on — how dark the dots are, how the vignette is
|
|
@@ -24,9 +24,9 @@
|
|
|
24
24
|
* remains.
|
|
25
25
|
*/
|
|
26
26
|
/**
|
|
27
|
-
* - `grid` — the house atmosphere: a dot-grid floor plus a corner vignette.
|
|
28
|
-
* - `none` — no texture
|
|
27
|
+
* - `grid` — the house atmosphere: a dot-grid floor plus a corner vignette. The default.
|
|
28
|
+
* - `none` — no texture: the region exactly as it rendered before the feature.
|
|
29
29
|
*/
|
|
30
30
|
export type ShellTexture = 'none' | 'grid';
|
|
31
|
-
/** Every texture, the untextured
|
|
31
|
+
/** Every texture, the untextured form first — the order it is documented in. */
|
|
32
32
|
export declare const SHELL_TEXTURES: readonly ["none", "grid"];
|
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
* it is a coin toss with a stylesheet.
|
|
13
13
|
*
|
|
14
14
|
* So it moves onto the shell, where there is exactly one place to say it and no
|
|
15
|
-
* per-route decision left to get wrong. `
|
|
16
|
-
*
|
|
15
|
+
* per-route decision left to get wrong. `grid` is the default: an opt-in house
|
|
16
|
+
* style measured who remembered it, not what the house looks like.
|
|
17
17
|
*
|
|
18
18
|
* It is one texture, not a menu. Both apps wanted the same picture, and the
|
|
19
19
|
* axes they actually differed on — how dark the dots are, how the vignette is
|
|
@@ -23,5 +23,5 @@
|
|
|
23
23
|
* second name in this union: set the grid ink to `transparent` and the vignette
|
|
24
24
|
* remains.
|
|
25
25
|
*/
|
|
26
|
-
/** Every texture, the untextured
|
|
26
|
+
/** Every texture, the untextured form first — the order it is documented in. */
|
|
27
27
|
export const SHELL_TEXTURES = ['none', 'grid'];
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
/** One step of the trail. With `href` it is a way back; without, it is where you are. */
|
|
3
|
+
export type BreadcrumbItem = { label: string; href?: string };
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<script lang="ts">
|
|
7
|
+
/**
|
|
8
|
+
* Where you are, as a trail of the routes above it. Three apps had written
|
|
9
|
+
* this by hand (one copy three times, one as a 28-call-site wrapper, one with
|
|
10
|
+
* slash separators), and none named the landmark, so a screen reader met an
|
|
11
|
+
* unlabelled list of links.
|
|
12
|
+
*
|
|
13
|
+
* The links are plain `<a href>`: SvelteKit routes them client-side, so a
|
|
14
|
+
* package with no router of its own can still render the app's routes, as
|
|
15
|
+
* AppNav does for `NavItem.href`.
|
|
16
|
+
*
|
|
17
|
+
* The trail never wraps. Each step truncates instead: an ancestor at 40% of
|
|
18
|
+
* its container (the viewport when there is none) up to 32rem, and three
|
|
19
|
+
* times as readily as where you are, so on a phone one long collection name
|
|
20
|
+
* gives way while short steps and the current one stay whole, and on a
|
|
21
|
+
* laptop it shows in full. Not a percentage: PageHeader's title column is
|
|
22
|
+
* as wide as its content, so 40% of it would truncate at any width.
|
|
23
|
+
*/
|
|
24
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
25
|
+
import ChevronRight from '@lucide/svelte/icons/chevron-right';
|
|
26
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
items,
|
|
30
|
+
label = 'Breadcrumb',
|
|
31
|
+
ref = $bindable(null),
|
|
32
|
+
class: className,
|
|
33
|
+
...restProps
|
|
34
|
+
}: WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
35
|
+
items: BreadcrumbItem[];
|
|
36
|
+
/** The landmark's accessible name. */
|
|
37
|
+
label?: string;
|
|
38
|
+
} = $props();
|
|
39
|
+
</script>
|
|
40
|
+
|
|
41
|
+
<nav
|
|
42
|
+
bind:this={ref}
|
|
43
|
+
aria-label={label}
|
|
44
|
+
data-slot="breadcrumbs"
|
|
45
|
+
class={cn('text-muted-foreground min-w-0 text-sm', className)}
|
|
46
|
+
{...restProps}
|
|
47
|
+
>
|
|
48
|
+
<ol class="flex min-w-0 items-center gap-1.5">
|
|
49
|
+
{#each items as item, i (i)}
|
|
50
|
+
{@const last = i === items.length - 1}
|
|
51
|
+
<li class={cn('flex min-w-0 items-center gap-1.5', !last && 'max-w-[min(40cqi,32rem)] shrink-[3]')}>
|
|
52
|
+
{#if i > 0}
|
|
53
|
+
<ChevronRight class="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
|
54
|
+
{/if}
|
|
55
|
+
{#if item.href}
|
|
56
|
+
<a
|
|
57
|
+
href={item.href}
|
|
58
|
+
class="hover:text-foreground focus-visible:ring-ring/50 min-w-0 truncate rounded-sm underline-offset-4 outline-none hover:underline focus-visible:ring-3"
|
|
59
|
+
>{item.label}</a
|
|
60
|
+
>
|
|
61
|
+
{:else}
|
|
62
|
+
<span
|
|
63
|
+
class="text-foreground min-w-0 truncate"
|
|
64
|
+
aria-current={last ? 'page' : undefined}>{item.label}</span
|
|
65
|
+
>
|
|
66
|
+
{/if}
|
|
67
|
+
</li>
|
|
68
|
+
{/each}
|
|
69
|
+
</ol>
|
|
70
|
+
</nav>
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** One step of the trail. With `href` it is a way back; without, it is where you are. */
|
|
2
|
+
export type BreadcrumbItem = {
|
|
3
|
+
label: string;
|
|
4
|
+
href?: string;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Where you are, as a trail of the routes above it. Three apps had written
|
|
8
|
+
* this by hand (one copy three times, one as a 28-call-site wrapper, one with
|
|
9
|
+
* slash separators), and none named the landmark, so a screen reader met an
|
|
10
|
+
* unlabelled list of links.
|
|
11
|
+
*
|
|
12
|
+
* The links are plain `<a href>`: SvelteKit routes them client-side, so a
|
|
13
|
+
* package with no router of its own can still render the app's routes, as
|
|
14
|
+
* AppNav does for `NavItem.href`.
|
|
15
|
+
*
|
|
16
|
+
* The trail never wraps. Each step truncates instead: an ancestor at 40% of
|
|
17
|
+
* its container (the viewport when there is none) up to 32rem, and three
|
|
18
|
+
* times as readily as where you are, so on a phone one long collection name
|
|
19
|
+
* gives way while short steps and the current one stay whole, and on a
|
|
20
|
+
* laptop it shows in full. Not a percentage: PageHeader's title column is
|
|
21
|
+
* as wide as its content, so 40% of it would truncate at any width.
|
|
22
|
+
*/
|
|
23
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
24
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
25
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLElement>> & {
|
|
26
|
+
items: BreadcrumbItem[];
|
|
27
|
+
/** The landmark's accessible name. */
|
|
28
|
+
label?: string;
|
|
29
|
+
};
|
|
30
|
+
declare const Breadcrumbs: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
31
|
+
type Breadcrumbs = ReturnType<typeof Breadcrumbs>;
|
|
32
|
+
export default Breadcrumbs;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
export type ContentSkeletonShape = 'rows' | 'tiles' | 'prose';
|
|
3
|
+
</script>
|
|
4
|
+
|
|
5
|
+
<script lang="ts">
|
|
6
|
+
/**
|
|
7
|
+
* A region of a page while its data loads, drawn in the shape of what will
|
|
8
|
+
* replace it: `rows` for a RecordList or a table, `tiles` for StatCards or a
|
|
9
|
+
* card grid, `prose` for a document body. The page renders its PageHeader
|
|
10
|
+
* for real and this in place of the region; a whole page still waiting on
|
|
11
|
+
* everything is LoadingState's (`sveltekit-ui-patterns.md` §Loading).
|
|
12
|
+
*
|
|
13
|
+
* Each shape is sized to the component it stands in for (a RecordList row is
|
|
14
|
+
* 2.5rem of text in 0.625rem of padding, a StatCard a label, a display
|
|
15
|
+
* figure and a line), so the swap moves nothing. One app had five sizes of
|
|
16
|
+
* bare Skeleton box for this, none matching what loaded.
|
|
17
|
+
*/
|
|
18
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
19
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
20
|
+
import Skeleton from '../skeleton/skeleton.svelte';
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
shape = 'rows',
|
|
24
|
+
count,
|
|
25
|
+
label = 'Loading…',
|
|
26
|
+
ref = $bindable(null),
|
|
27
|
+
class: className,
|
|
28
|
+
...restProps
|
|
29
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
30
|
+
shape?: ContentSkeletonShape;
|
|
31
|
+
/** Rows, tiles, or paragraphs. Defaults to 5, 4 and 3. */
|
|
32
|
+
count?: number;
|
|
33
|
+
/** What a screen reader hears while it stands. */
|
|
34
|
+
label?: string;
|
|
35
|
+
} = $props();
|
|
36
|
+
|
|
37
|
+
const n = $derived(count ?? { rows: 5, tiles: 4, prose: 3 }[shape]);
|
|
38
|
+
// Varied, but fixed: a skeleton that changed width on every render would
|
|
39
|
+
// read as motion.
|
|
40
|
+
const TITLE = ['w-2/5', 'w-3/5', 'w-1/2', 'w-2/3', 'w-1/3'];
|
|
41
|
+
const NOTE = ['w-1/4', 'w-1/3', 'w-1/5', 'w-2/5'];
|
|
42
|
+
const LAST_LINE = ['w-3/5', 'w-2/5', 'w-4/5'];
|
|
43
|
+
</script>
|
|
44
|
+
|
|
45
|
+
<div
|
|
46
|
+
bind:this={ref}
|
|
47
|
+
role="status"
|
|
48
|
+
aria-live="polite"
|
|
49
|
+
aria-busy="true"
|
|
50
|
+
data-slot="content-skeleton"
|
|
51
|
+
data-shape={shape}
|
|
52
|
+
class={cn('@container min-w-0', className)}
|
|
53
|
+
{...restProps}
|
|
54
|
+
>
|
|
55
|
+
<span class="sr-only">{label}</span>
|
|
56
|
+
{#if shape === 'rows'}
|
|
57
|
+
<div class="bg-card border-border ds-edge divide-border divide-y rounded-lg border" aria-hidden="true">
|
|
58
|
+
{#each { length: n }, i (i)}
|
|
59
|
+
<div class="flex flex-col gap-1 px-3 py-2.5">
|
|
60
|
+
<div class="flex h-5 items-center">
|
|
61
|
+
<Skeleton class={cn('h-3.5', TITLE[i % TITLE.length])} />
|
|
62
|
+
</div>
|
|
63
|
+
<div class="flex h-4 items-center">
|
|
64
|
+
<Skeleton class={cn('h-3', NOTE[i % NOTE.length])} />
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
{/each}
|
|
68
|
+
</div>
|
|
69
|
+
{:else if shape === 'tiles'}
|
|
70
|
+
<div class="grid grid-cols-1 gap-4 @sm:grid-cols-2 @3xl:grid-cols-3 @5xl:grid-cols-4" aria-hidden="true">
|
|
71
|
+
{#each { length: n }, i (i)}
|
|
72
|
+
<div class="bg-card border-border ds-edge flex flex-col gap-1.5 rounded-lg border p-4">
|
|
73
|
+
<div class="flex h-4 items-center"><Skeleton class="h-2.5 w-1/3" /></div>
|
|
74
|
+
<div class="flex h-[2.125rem] items-center"><Skeleton class="h-6 w-1/2" /></div>
|
|
75
|
+
<div class="flex h-4 items-center">
|
|
76
|
+
<Skeleton class={cn('h-3', NOTE[i % NOTE.length])} />
|
|
77
|
+
</div>
|
|
78
|
+
</div>
|
|
79
|
+
{/each}
|
|
80
|
+
</div>
|
|
81
|
+
{:else}
|
|
82
|
+
<div class="ds-measure flex flex-col gap-5" data-measure="prose" aria-hidden="true">
|
|
83
|
+
{#each { length: n }, i (i)}
|
|
84
|
+
<div class="flex flex-col">
|
|
85
|
+
{#each { length: 4 }, line (line)}
|
|
86
|
+
<div class="flex h-6 items-center">
|
|
87
|
+
<Skeleton class={cn('h-3.5', line === 3 ? LAST_LINE[i % LAST_LINE.length] : 'w-full')} />
|
|
88
|
+
</div>
|
|
89
|
+
{/each}
|
|
90
|
+
</div>
|
|
91
|
+
{/each}
|
|
92
|
+
</div>
|
|
93
|
+
{/if}
|
|
94
|
+
</div>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type ContentSkeletonShape = 'rows' | 'tiles' | 'prose';
|
|
2
|
+
/**
|
|
3
|
+
* A region of a page while its data loads, drawn in the shape of what will
|
|
4
|
+
* replace it: `rows` for a RecordList or a table, `tiles` for StatCards or a
|
|
5
|
+
* card grid, `prose` for a document body. The page renders its PageHeader
|
|
6
|
+
* for real and this in place of the region; a whole page still waiting on
|
|
7
|
+
* everything is LoadingState's (`sveltekit-ui-patterns.md` §Loading).
|
|
8
|
+
*
|
|
9
|
+
* Each shape is sized to the component it stands in for (a RecordList row is
|
|
10
|
+
* 2.5rem of text in 0.625rem of padding, a StatCard a label, a display
|
|
11
|
+
* figure and a line), so the swap moves nothing. One app had five sizes of
|
|
12
|
+
* bare Skeleton box for this, none matching what loaded.
|
|
13
|
+
*/
|
|
14
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
15
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
16
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
17
|
+
shape?: ContentSkeletonShape;
|
|
18
|
+
/** Rows, tiles, or paragraphs. Defaults to 5, 4 and 3. */
|
|
19
|
+
count?: number;
|
|
20
|
+
/** What a screen reader hears while it stands. */
|
|
21
|
+
label?: string;
|
|
22
|
+
};
|
|
23
|
+
declare const ContentSkeleton: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
24
|
+
type ContentSkeleton = ReturnType<typeof ContentSkeleton>;
|
|
25
|
+
export default ContentSkeleton;
|