@poodle64/ui 2026.10.17 → 2026.10.19
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 +101 -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/graph-canvas/graph-canvas.svelte +69 -2
- package/dist/components/ui/graph-canvas/graph-canvas.svelte.d.ts +2 -0
- package/dist/components/ui/graph-canvas/scene.d.ts +6 -0
- package/dist/components/ui/graph-canvas/scene.js +52 -21
- package/dist/components/ui/graph-canvas/types.d.ts +3 -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 +22 -0
- package/package.json +2 -1
- package/registry/component-map.json +43 -4
- package/registry/component-map.md +9 -4
|
@@ -34,6 +34,10 @@ export class GraphScene {
|
|
|
34
34
|
selected = null;
|
|
35
35
|
hovered = null;
|
|
36
36
|
matches = new Set();
|
|
37
|
+
/** The highlighted route: its nodes and every edge joining consecutive ones. */
|
|
38
|
+
routeNodes = new Set();
|
|
39
|
+
routeEdges = new Set();
|
|
40
|
+
routeIds = [];
|
|
37
41
|
sim;
|
|
38
42
|
link = forceLink([]);
|
|
39
43
|
charge = forceManyBody().strength((d) => -28 - 14 * Math.sqrt(d.degree));
|
|
@@ -143,8 +147,20 @@ export class GraphScene {
|
|
|
143
147
|
if (this.hovered)
|
|
144
148
|
this.hovered = byId.get(this.hovered.id) ?? null;
|
|
145
149
|
this.matches = new Set([...this.matches].flatMap((m) => byId.get(m.id) ?? []));
|
|
150
|
+
this.setRoute(this.routeIds);
|
|
146
151
|
return fresh.length;
|
|
147
152
|
}
|
|
153
|
+
/** Highlight a path given as node ids in order; ids not in the graph are skipped. */
|
|
154
|
+
setRoute(ids) {
|
|
155
|
+
this.routeIds = ids;
|
|
156
|
+
const nodes = ids.flatMap((id) => this.byId.get(id) ?? []);
|
|
157
|
+
this.routeNodes = new Set(nodes);
|
|
158
|
+
const key = (a, b) => (a < b ? `${a}\u0000${b}` : `${b}\u0000${a}`);
|
|
159
|
+
const pairs = new Set();
|
|
160
|
+
for (let i = 1; i < nodes.length; i++)
|
|
161
|
+
pairs.add(key(nodes[i - 1].id, nodes[i].id));
|
|
162
|
+
this.routeEdges = new Set(this.edges.filter((e) => pairs.has(key(e.source.id, e.target.id))));
|
|
163
|
+
}
|
|
148
164
|
hot() {
|
|
149
165
|
return this.sim.alpha() >= this.sim.alphaMin();
|
|
150
166
|
}
|
|
@@ -236,9 +252,12 @@ export class GraphScene {
|
|
|
236
252
|
if (!P)
|
|
237
253
|
return;
|
|
238
254
|
const chosen = this.selected && this.visible(this.selected) ? this.selected : null;
|
|
239
|
-
|
|
255
|
+
// A route holds the picture until the pointer rests on a node.
|
|
256
|
+
const route = !this.hovered && this.routeNodes.size > 0;
|
|
257
|
+
const focus = route ? null : (this.hovered ?? chosen);
|
|
240
258
|
const near = focus ? this.adj.get(focus) : undefined;
|
|
241
|
-
const
|
|
259
|
+
const dim = !!focus || route;
|
|
260
|
+
const lit = (s) => (route ? this.routeNodes.has(s) : !focus || s === focus || !!near?.has(s));
|
|
242
261
|
const margin = 40 / k;
|
|
243
262
|
const vx0 = -tx / k - margin;
|
|
244
263
|
const vy0 = -ty / k - margin;
|
|
@@ -268,24 +287,26 @@ export class GraphScene {
|
|
|
268
287
|
const curved = shown.length < 1200;
|
|
269
288
|
// Edges: one batched path for the rest, one per colour for the lit ones.
|
|
270
289
|
const rest = new Path2D();
|
|
290
|
+
const restDashed = new Path2D();
|
|
271
291
|
const restHeads = new Path2D();
|
|
272
292
|
const litPaths = new Map();
|
|
273
293
|
const edgeLabels = [];
|
|
274
|
-
const heads = this.directed && (shown.length < 500 ||
|
|
294
|
+
const heads = this.directed && (shown.length < 500 || dim);
|
|
275
295
|
// A hub's relations crowd its own spokes; past a dozen the card lists them instead.
|
|
276
296
|
const relationWords = focus ? (this.adj.get(focus)?.size ?? 0) <= 12 : shown.length < 300;
|
|
277
297
|
for (const e of shown) {
|
|
278
298
|
const a = e.source;
|
|
279
299
|
const b = e.target;
|
|
280
|
-
const hot = !!focus && (a === focus || b === focus);
|
|
281
|
-
|
|
300
|
+
const hot = route ? this.routeEdges.has(e) : !!focus && (a === focus || b === focus);
|
|
301
|
+
const dashed = e.edge.style === 'dashed';
|
|
302
|
+
let path = dashed ? restDashed : rest;
|
|
282
303
|
let head = restHeads;
|
|
283
304
|
if (hot) {
|
|
284
|
-
const colour = this.colour(a === focus ? b : a);
|
|
305
|
+
const colour = route ? P.ink : this.colour(a === focus ? b : a);
|
|
285
306
|
let p = litPaths.get(colour);
|
|
286
307
|
if (!p)
|
|
287
|
-
litPaths.set(colour, (p = { line: new Path2D(), head: new Path2D() }));
|
|
288
|
-
path = p.line;
|
|
308
|
+
litPaths.set(colour, (p = { line: new Path2D(), dashed: new Path2D(), head: new Path2D() }));
|
|
309
|
+
path = dashed ? p.dashed : p.line;
|
|
289
310
|
head = p.head;
|
|
290
311
|
}
|
|
291
312
|
const [cx, cy] = curved || hot ? this.control(e) : [(a.x + b.x) / 2, (a.y + b.y) / 2];
|
|
@@ -294,30 +315,36 @@ export class GraphScene {
|
|
|
294
315
|
path.quadraticCurveTo(cx, cy, b.x, b.y);
|
|
295
316
|
else
|
|
296
317
|
path.lineTo(b.x, b.y);
|
|
297
|
-
if (heads && (hot || !
|
|
318
|
+
if (heads && (hot || !dim))
|
|
298
319
|
this.arrow(head, cx, cy, b, rad(b), k);
|
|
299
|
-
if (e.edge.label && relationWords && (hot || !focus))
|
|
320
|
+
if (e.edge.label && (route ? hot : relationWords && (hot || !focus)))
|
|
300
321
|
edgeLabels.push(e);
|
|
301
322
|
}
|
|
302
323
|
ctx.strokeStyle = P.edge;
|
|
303
324
|
ctx.fillStyle = P.edge;
|
|
304
|
-
ctx.globalAlpha =
|
|
325
|
+
ctx.globalAlpha = dim ? 0.07 : Math.min(0.65, 0.12 + 0.35 * k);
|
|
305
326
|
ctx.lineWidth = 1 / k;
|
|
306
327
|
ctx.stroke(rest);
|
|
328
|
+
ctx.setLineDash([5 / k, 4 / k]);
|
|
329
|
+
ctx.stroke(restDashed);
|
|
330
|
+
ctx.setLineDash([]);
|
|
307
331
|
ctx.fill(restHeads);
|
|
308
332
|
ctx.globalAlpha = 0.95;
|
|
309
|
-
ctx.lineWidth = 1.75 / k;
|
|
333
|
+
ctx.lineWidth = (route ? 2.75 : 1.75) / k;
|
|
310
334
|
for (const [colour, p] of litPaths) {
|
|
311
335
|
ctx.strokeStyle = colour;
|
|
312
336
|
ctx.fillStyle = colour;
|
|
313
337
|
ctx.stroke(p.line);
|
|
338
|
+
ctx.setLineDash([6 / k, 4 / k]);
|
|
339
|
+
ctx.stroke(p.dashed);
|
|
340
|
+
ctx.setLineDash([]);
|
|
314
341
|
ctx.fill(p.head);
|
|
315
342
|
}
|
|
316
343
|
// Nodes: dimmed first, then lit, each batched by kind. A dot too small to
|
|
317
344
|
// overlap its neighbour visibly skips the ground-coloured halo.
|
|
318
345
|
const halo = k >= 0.5 ? 1.4 / k : 0;
|
|
319
346
|
for (const pass of [false, true]) {
|
|
320
|
-
if (pass === false && !
|
|
347
|
+
if (pass === false && !dim)
|
|
321
348
|
continue;
|
|
322
349
|
const groups = new Map();
|
|
323
350
|
for (const s of this.nodes) {
|
|
@@ -345,7 +372,8 @@ export class GraphScene {
|
|
|
345
372
|
if (!this.visible(s) || !onScreen(s))
|
|
346
373
|
continue;
|
|
347
374
|
const ring = s === this.selected || s === focus;
|
|
348
|
-
|
|
375
|
+
const onRoute = route && this.routeNodes.has(s);
|
|
376
|
+
if (!s.node.emphasis && !ring && !onRoute && !this.matches.has(s))
|
|
349
377
|
continue;
|
|
350
378
|
const r = rad(s);
|
|
351
379
|
const shape = this.shapes.get(s.node.kind) ?? 'circle';
|
|
@@ -361,11 +389,11 @@ export class GraphScene {
|
|
|
361
389
|
const p = new Path2D();
|
|
362
390
|
this.shape(p, shape, s.x, s.y, r + 3.5 / k);
|
|
363
391
|
ctx.lineWidth = (ring ? 2 : 1.4) / k;
|
|
364
|
-
ctx.strokeStyle = ring || this.matches.has(s) ? P.ink : this.colour(s);
|
|
392
|
+
ctx.strokeStyle = ring || onRoute || this.matches.has(s) ? P.ink : this.colour(s);
|
|
365
393
|
ctx.stroke(p);
|
|
366
394
|
}
|
|
367
395
|
ctx.globalAlpha = 1;
|
|
368
|
-
this.labels(ctx, P, focus, near, edgeLabels, onScreen, rad);
|
|
396
|
+
this.labels(ctx, P, focus, near, edgeLabels, route, onScreen, rad);
|
|
369
397
|
}
|
|
370
398
|
colour(s) {
|
|
371
399
|
return this.palette?.kinds.get(s.node.kind) ?? this.palette?.fallback ?? 'currentColor';
|
|
@@ -457,7 +485,7 @@ export class GraphScene {
|
|
|
457
485
|
}
|
|
458
486
|
return true;
|
|
459
487
|
}
|
|
460
|
-
labels(ctx, P, focus, near, edgeLabels, onScreen, rad) {
|
|
488
|
+
labels(ctx, P, focus, near, edgeLabels, route, onScreen, rad) {
|
|
461
489
|
const { k, x: tx, y: ty } = this.cam;
|
|
462
490
|
const { dpr, width: W, height: H } = this;
|
|
463
491
|
this.grid.clear();
|
|
@@ -487,8 +515,11 @@ export class GraphScene {
|
|
|
487
515
|
else {
|
|
488
516
|
if (this.selected)
|
|
489
517
|
order.push(this.selected);
|
|
518
|
+
if (route)
|
|
519
|
+
order.push(...this.routeNodes);
|
|
490
520
|
order.push(...this.matches);
|
|
491
|
-
|
|
521
|
+
if (!route)
|
|
522
|
+
order.push(...this.ranked);
|
|
492
523
|
}
|
|
493
524
|
const seen = new Set();
|
|
494
525
|
const quota = Math.ceil((W * H) / LABEL_AREA);
|
|
@@ -504,7 +535,7 @@ export class GraphScene {
|
|
|
504
535
|
seen.add(s);
|
|
505
536
|
tries++;
|
|
506
537
|
const text = clip(s.node.label);
|
|
507
|
-
const prominent = s === focus || s === this.selected || !!s.node.emphasis;
|
|
538
|
+
const prominent = s === focus || s === this.selected || !!s.node.emphasis || (route && this.routeNodes.has(s));
|
|
508
539
|
if (!focus && placed >= quota && rad(s) * k < 7 && !prominent && !this.matches.has(s))
|
|
509
540
|
continue;
|
|
510
541
|
const font = prominent ? strong : body;
|
|
@@ -519,7 +550,7 @@ export class GraphScene {
|
|
|
519
550
|
const below = ty + s.y * k + r + 4;
|
|
520
551
|
const above = ty + s.y * k - r - 4 - h;
|
|
521
552
|
const fits = (y) => this.claim(sx - w / 2 - 3, y - 2, sx + w / 2 + 3, y + h + 2);
|
|
522
|
-
const sy = fits(below) ? below : fits(above) ? above : s === focus || s === this.selected ? below : null;
|
|
553
|
+
const sy = fits(below) ? below : fits(above) ? above : s === focus || s === this.selected || (route && this.routeNodes.has(s)) ? below : null;
|
|
523
554
|
if (sy === null || sy > H || sy + h < 0)
|
|
524
555
|
continue;
|
|
525
556
|
this.text(ctx, font, text, sx, sy, 'top', P.ink, P.ground);
|
|
@@ -542,7 +573,7 @@ export class GraphScene {
|
|
|
542
573
|
const w = this.measure(ctx, small, text);
|
|
543
574
|
if (!this.claim(mx - w / 2 - 3, my - 7, mx + w / 2 + 3, my + 7))
|
|
544
575
|
continue;
|
|
545
|
-
const hot = !!focus && (e.source === focus || e.target === focus);
|
|
576
|
+
const hot = route || (!!focus && (e.source === focus || e.target === focus));
|
|
546
577
|
this.text(ctx, small, text, mx, my, 'middle', hot ? P.ink : P.inkSoft, P.ground);
|
|
547
578
|
edgesPlaced++;
|
|
548
579
|
}
|
|
@@ -14,7 +14,10 @@ export interface GraphNode {
|
|
|
14
14
|
export interface GraphEdge {
|
|
15
15
|
source: string;
|
|
16
16
|
target: string;
|
|
17
|
+
/** The relation's verb, drawn on the line on the highlighted path or around a focused node. */
|
|
17
18
|
label?: string;
|
|
19
|
+
/** `dashed` for a weaker link, such as a claim with no citation yet. Defaults to `solid`. */
|
|
20
|
+
style?: 'solid' | 'dashed';
|
|
18
21
|
}
|
|
19
22
|
export type GraphShape = 'circle' | 'square' | 'diamond' | 'triangle';
|
|
20
23
|
/** A legend row. The colour is any CSS colour; a `var(--…)` resolves against the graph's own element. */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A route's page: its header, sections and panels stacked at the one page
|
|
4
|
+
* rhythm. Pages had written that wrapper per file (`space-y-6`, `space-y-10`,
|
|
5
|
+
* `gap-4`, `gap-5`, `gap-10` in one app; a local PageContainer behind 56
|
|
6
|
+
* call sites in another).
|
|
7
|
+
*
|
|
8
|
+
* Width is not this component's: AppShell caps the content box from the
|
|
9
|
+
* route's `measure`, which a route declares in its load data. The container
|
|
10
|
+
* fills that box and grows to its height, so a page holding a board or a
|
|
11
|
+
* table can scroll inside itself.
|
|
12
|
+
*
|
|
13
|
+
* PageHeader carries its own bottom margin for pages written without this;
|
|
14
|
+
* inside it the gap alone spaces the header, so the header sits at the same
|
|
15
|
+
* distance either way.
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
19
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
20
|
+
|
|
21
|
+
let {
|
|
22
|
+
children,
|
|
23
|
+
ref = $bindable(null),
|
|
24
|
+
class: className,
|
|
25
|
+
...restProps
|
|
26
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & { children: Snippet } = $props();
|
|
27
|
+
</script>
|
|
28
|
+
|
|
29
|
+
<div
|
|
30
|
+
bind:this={ref}
|
|
31
|
+
data-slot="page-container"
|
|
32
|
+
class={cn(
|
|
33
|
+
'flex min-h-0 w-full min-w-0 flex-1 flex-col gap-6 *:data-[slot=page-header]:mb-0',
|
|
34
|
+
className
|
|
35
|
+
)}
|
|
36
|
+
{...restProps}
|
|
37
|
+
>
|
|
38
|
+
{@render children()}
|
|
39
|
+
</div>
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A route's page: its header, sections and panels stacked at the one page
|
|
3
|
+
* rhythm. Pages had written that wrapper per file (`space-y-6`, `space-y-10`,
|
|
4
|
+
* `gap-4`, `gap-5`, `gap-10` in one app; a local PageContainer behind 56
|
|
5
|
+
* call sites in another).
|
|
6
|
+
*
|
|
7
|
+
* Width is not this component's: AppShell caps the content box from the
|
|
8
|
+
* route's `measure`, which a route declares in its load data. The container
|
|
9
|
+
* fills that box and grows to its height, so a page holding a board or a
|
|
10
|
+
* table can scroll inside itself.
|
|
11
|
+
*
|
|
12
|
+
* PageHeader carries its own bottom margin for pages written without this;
|
|
13
|
+
* inside it the gap alone spaces the header, so the header sits at the same
|
|
14
|
+
* distance either way.
|
|
15
|
+
*/
|
|
16
|
+
import type { Snippet } from 'svelte';
|
|
17
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
18
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
19
|
+
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
20
|
+
children: Snippet;
|
|
21
|
+
};
|
|
22
|
+
declare const PageContainer: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
23
|
+
type PageContainer = ReturnType<typeof PageContainer>;
|
|
24
|
+
export default PageContainer;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
4
4
|
import { cn, type WithElementRef } from '../../../utils.js';
|
|
5
5
|
import InfoTip from '../info-tip/info-tip.svelte';
|
|
6
|
+
import Breadcrumbs, { type BreadcrumbItem } from '../breadcrumbs/breadcrumbs.svelte';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* The one page-title pattern for every route. `subtitle` is a short, single
|
|
@@ -23,10 +24,11 @@
|
|
|
23
24
|
* the drift this package exists to end. One component, one spacing rhythm,
|
|
24
25
|
* two shapes.
|
|
25
26
|
*
|
|
26
|
-
* The trail
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
27
|
+
* The trail is a list of steps (`BreadcrumbItem[]`), drawn by Breadcrumbs.
|
|
28
|
+
* A snippet is still accepted for a trail that is not a list of links (a
|
|
29
|
+
* record switcher in the trail's place), but a list is the default: three
|
|
30
|
+
* apps wrote the same chevron trail by hand, one behind a 28-call-site
|
|
31
|
+
* wrapper that existed only to turn this snippet into an array.
|
|
30
32
|
*
|
|
31
33
|
* `icon` and `meta` are snippets for the same reason and earn their place the
|
|
32
34
|
* same way `breadcrumbs` did: three apps had hand-rolled the meta row and two
|
|
@@ -50,8 +52,8 @@
|
|
|
50
52
|
}: WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
51
53
|
/** A short kicker above the title. Use this OR `breadcrumbs`, not both. */
|
|
52
54
|
eyebrow?: string;
|
|
53
|
-
/** The trail above the title
|
|
54
|
-
breadcrumbs?: Snippet;
|
|
55
|
+
/** The trail above the title: its steps, or a snippet for a trail that is not links. */
|
|
56
|
+
breadcrumbs?: BreadcrumbItem[] | Snippet;
|
|
55
57
|
/** The glyph alone. The tinted square around it is this component's. */
|
|
56
58
|
icon?: Snippet;
|
|
57
59
|
/** Omit for a header that is a breadcrumb bar with actions. */
|
|
@@ -66,6 +68,7 @@
|
|
|
66
68
|
|
|
67
69
|
<div
|
|
68
70
|
bind:this={ref}
|
|
71
|
+
data-slot="page-header"
|
|
69
72
|
class={cn('mb-6 flex flex-wrap items-start justify-between gap-4', className)}
|
|
70
73
|
{...restProps}
|
|
71
74
|
>
|
|
@@ -83,10 +86,12 @@
|
|
|
83
86
|
</span>
|
|
84
87
|
{/if}
|
|
85
88
|
<div class="min-w-0">
|
|
86
|
-
{#if breadcrumbs}
|
|
89
|
+
{#if typeof breadcrumbs === 'function'}
|
|
87
90
|
<div class="text-muted-foreground mb-1.5 flex min-w-0 items-center text-sm">
|
|
88
91
|
{@render breadcrumbs()}
|
|
89
92
|
</div>
|
|
93
|
+
{:else if breadcrumbs?.length}
|
|
94
|
+
<Breadcrumbs items={breadcrumbs} class="mb-1.5" />
|
|
90
95
|
{/if}
|
|
91
96
|
{#if eyebrow}
|
|
92
97
|
<div class="text-primary text-2xs tracking-eyebrow mb-1 font-medium uppercase">
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
2
|
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
3
|
import { type WithElementRef } from '../../../utils.js';
|
|
4
|
+
import { type BreadcrumbItem } from '../breadcrumbs/breadcrumbs.svelte';
|
|
4
5
|
type $$ComponentProps = WithElementRef<HTMLAttributes<HTMLDivElement>> & {
|
|
5
6
|
/** A short kicker above the title. Use this OR `breadcrumbs`, not both. */
|
|
6
7
|
eyebrow?: string;
|
|
7
|
-
/** The trail above the title
|
|
8
|
-
breadcrumbs?: Snippet;
|
|
8
|
+
/** The trail above the title: its steps, or a snippet for a trail that is not links. */
|
|
9
|
+
breadcrumbs?: BreadcrumbItem[] | Snippet;
|
|
9
10
|
/** The glyph alone. The tinted square around it is this component's. */
|
|
10
11
|
icon?: Snippet;
|
|
11
12
|
/** Omit for a header that is a breadcrumb bar with actions. */
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* A section of a page, under its PageHeader: a heading, an optional line of
|
|
4
|
+
* explanation, trailing actions, and the section's content at one distance
|
|
5
|
+
* below them.
|
|
6
|
+
*
|
|
7
|
+
* One app had five recipes for this one role, from an 11px uppercase kicker
|
|
8
|
+
* to a 20px display heading, and each page spaced its content from the
|
|
9
|
+
* heading differently. The heading and the gap under it are this
|
|
10
|
+
* component's, because those are the halves that drift.
|
|
11
|
+
*
|
|
12
|
+
* Level 2 is a section, in the display face one step under the page title.
|
|
13
|
+
* Level 3 is a subsection, in the body face at a Panel title's size, so a
|
|
14
|
+
* titled card and a subsection read as the same rank. There is no uppercase
|
|
15
|
+
* kicker form: a small caps label above content is the column head of a
|
|
16
|
+
* table or a nav group, never a heading.
|
|
17
|
+
*/
|
|
18
|
+
import type { Snippet } from 'svelte';
|
|
19
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
20
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
title,
|
|
24
|
+
level = 2,
|
|
25
|
+
description,
|
|
26
|
+
count,
|
|
27
|
+
actions,
|
|
28
|
+
children,
|
|
29
|
+
id,
|
|
30
|
+
ref = $bindable(null),
|
|
31
|
+
class: className,
|
|
32
|
+
...restProps
|
|
33
|
+
}: Omit<WithElementRef<HTMLAttributes<HTMLElement>>, 'title'> & {
|
|
34
|
+
title: string;
|
|
35
|
+
/** 2, a section of the page; 3, a section within one. */
|
|
36
|
+
level?: 2 | 3;
|
|
37
|
+
/** A sentence or two under the heading. It wraps. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/** How many things the section holds, set beside the heading. */
|
|
40
|
+
count?: number;
|
|
41
|
+
/** Controls for the section, trailing the heading. */
|
|
42
|
+
actions?: Snippet;
|
|
43
|
+
children?: Snippet;
|
|
44
|
+
/** The heading's id, for a link to the section. One is generated otherwise. */
|
|
45
|
+
id?: string;
|
|
46
|
+
} = $props();
|
|
47
|
+
|
|
48
|
+
const fallbackId = $props.id();
|
|
49
|
+
const headingId = $derived(id ?? `section-${fallbackId}`);
|
|
50
|
+
</script>
|
|
51
|
+
|
|
52
|
+
<section
|
|
53
|
+
bind:this={ref}
|
|
54
|
+
aria-labelledby={headingId}
|
|
55
|
+
data-slot="page-section"
|
|
56
|
+
class={cn('flex min-w-0 flex-col', level === 2 ? 'gap-4' : 'gap-3', className)}
|
|
57
|
+
{...restProps}
|
|
58
|
+
>
|
|
59
|
+
<!-- Actions share the heading's row, so on a phone they stay beside the
|
|
60
|
+
heading rather than dropping under the description. -->
|
|
61
|
+
<header class="flex min-w-0 flex-col gap-1">
|
|
62
|
+
<div class="flex min-w-0 flex-wrap items-center gap-x-4 gap-y-2">
|
|
63
|
+
<svelte:element
|
|
64
|
+
this={`h${level}`}
|
|
65
|
+
id={headingId}
|
|
66
|
+
class={cn(
|
|
67
|
+
'flex min-w-0 items-baseline gap-2 scroll-mt-4 font-semibold',
|
|
68
|
+
level === 2
|
|
69
|
+
? 'font-display text-section tracking-[var(--ds-font-display-tracking,-0.015em)]'
|
|
70
|
+
: 'text-body leading-tight tracking-tight'
|
|
71
|
+
)}
|
|
72
|
+
>
|
|
73
|
+
<span class="min-w-0">{title}</span>
|
|
74
|
+
{#if count !== undefined}
|
|
75
|
+
<span
|
|
76
|
+
class="text-muted-foreground font-sans text-sm font-normal tracking-normal tabular-nums"
|
|
77
|
+
>{count}</span
|
|
78
|
+
>
|
|
79
|
+
{/if}
|
|
80
|
+
</svelte:element>
|
|
81
|
+
{#if actions}
|
|
82
|
+
<div class="ml-auto flex min-w-0 flex-wrap items-center justify-end gap-2">
|
|
83
|
+
{@render actions()}
|
|
84
|
+
</div>
|
|
85
|
+
{/if}
|
|
86
|
+
</div>
|
|
87
|
+
{#if description}
|
|
88
|
+
<p class="text-muted-foreground ds-measure text-sm" data-measure="prose">{description}</p>
|
|
89
|
+
{/if}
|
|
90
|
+
</header>
|
|
91
|
+
{@render children?.()}
|
|
92
|
+
</section>
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A section of a page, under its PageHeader: a heading, an optional line of
|
|
3
|
+
* explanation, trailing actions, and the section's content at one distance
|
|
4
|
+
* below them.
|
|
5
|
+
*
|
|
6
|
+
* One app had five recipes for this one role, from an 11px uppercase kicker
|
|
7
|
+
* to a 20px display heading, and each page spaced its content from the
|
|
8
|
+
* heading differently. The heading and the gap under it are this
|
|
9
|
+
* component's, because those are the halves that drift.
|
|
10
|
+
*
|
|
11
|
+
* Level 2 is a section, in the display face one step under the page title.
|
|
12
|
+
* Level 3 is a subsection, in the body face at a Panel title's size, so a
|
|
13
|
+
* titled card and a subsection read as the same rank. There is no uppercase
|
|
14
|
+
* kicker form: a small caps label above content is the column head of a
|
|
15
|
+
* table or a nav group, never a heading.
|
|
16
|
+
*/
|
|
17
|
+
import type { Snippet } from 'svelte';
|
|
18
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
19
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
20
|
+
type $$ComponentProps = Omit<WithElementRef<HTMLAttributes<HTMLElement>>, 'title'> & {
|
|
21
|
+
title: string;
|
|
22
|
+
/** 2, a section of the page; 3, a section within one. */
|
|
23
|
+
level?: 2 | 3;
|
|
24
|
+
/** A sentence or two under the heading. It wraps. */
|
|
25
|
+
description?: string;
|
|
26
|
+
/** How many things the section holds, set beside the heading. */
|
|
27
|
+
count?: number;
|
|
28
|
+
/** Controls for the section, trailing the heading. */
|
|
29
|
+
actions?: Snippet;
|
|
30
|
+
children?: Snippet;
|
|
31
|
+
/** The heading's id, for a link to the section. One is generated otherwise. */
|
|
32
|
+
id?: string;
|
|
33
|
+
};
|
|
34
|
+
declare const PageSection: import("svelte").Component<$$ComponentProps, {}, "ref">;
|
|
35
|
+
type PageSection = ReturnType<typeof PageSection>;
|
|
36
|
+
export default PageSection;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Root from './sheet.svelte';
|
|
2
|
+
import Trigger from './sheet-trigger.svelte';
|
|
3
|
+
import Close from './sheet-close.svelte';
|
|
4
|
+
import Portal from './sheet-portal.svelte';
|
|
5
|
+
import Overlay from './sheet-overlay.svelte';
|
|
6
|
+
import Content from './sheet-content.svelte';
|
|
7
|
+
import Header from './sheet-header.svelte';
|
|
8
|
+
import Body from './sheet-body.svelte';
|
|
9
|
+
import Footer from './sheet-footer.svelte';
|
|
10
|
+
import Title from './sheet-title.svelte';
|
|
11
|
+
import Description from './sheet-description.svelte';
|
|
12
|
+
export { sheetVariants, type SheetSide } from './sheet-content.svelte';
|
|
13
|
+
export { Root, Trigger, Close, Portal, Overlay, Content, Header, Body, Footer, Title, Description, Root as Sheet, Trigger as SheetTrigger, Close as SheetClose, Portal as SheetPortal, Overlay as SheetOverlay, Content as SheetContent, Header as SheetHeader, Body as SheetBody, Footer as SheetFooter, Title as SheetTitle, Description as SheetDescription };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import Root from './sheet.svelte';
|
|
2
|
+
import Trigger from './sheet-trigger.svelte';
|
|
3
|
+
import Close from './sheet-close.svelte';
|
|
4
|
+
import Portal from './sheet-portal.svelte';
|
|
5
|
+
import Overlay from './sheet-overlay.svelte';
|
|
6
|
+
import Content from './sheet-content.svelte';
|
|
7
|
+
import Header from './sheet-header.svelte';
|
|
8
|
+
import Body from './sheet-body.svelte';
|
|
9
|
+
import Footer from './sheet-footer.svelte';
|
|
10
|
+
import Title from './sheet-title.svelte';
|
|
11
|
+
import Description from './sheet-description.svelte';
|
|
12
|
+
export { sheetVariants } from './sheet-content.svelte';
|
|
13
|
+
export { Root, Trigger, Close, Portal, Overlay, Content, Header, Body, Footer, Title, Description,
|
|
14
|
+
//
|
|
15
|
+
Root as Sheet, Trigger as SheetTrigger, Close as SheetClose, Portal as SheetPortal, Overlay as SheetOverlay, Content as SheetContent, Header as SheetHeader, Body as SheetBody, Footer as SheetFooter, Title as SheetTitle, Description as SheetDescription };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
3
|
+
import { cn, type WithElementRef } from '../../../utils.js';
|
|
4
|
+
|
|
5
|
+
let {
|
|
6
|
+
ref = $bindable(null),
|
|
7
|
+
class: className,
|
|
8
|
+
children,
|
|
9
|
+
...restProps
|
|
10
|
+
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<!-- The part that scrolls, so the header and footer stay put. -->
|
|
14
|
+
<div
|
|
15
|
+
bind:this={ref}
|
|
16
|
+
data-slot="sheet-body"
|
|
17
|
+
class={cn('min-h-0 flex-1 overflow-y-auto p-4', className)}
|
|
18
|
+
{...restProps}
|
|
19
|
+
>
|
|
20
|
+
{@render children?.()}
|
|
21
|
+
</div>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { HTMLAttributes } from 'svelte/elements';
|
|
2
|
+
import { type WithElementRef } from '../../../utils.js';
|
|
3
|
+
declare const SheetBody: import("svelte").Component<WithElementRef<HTMLAttributes<HTMLDivElement>>, {}, "ref">;
|
|
4
|
+
type SheetBody = ReturnType<typeof SheetBody>;
|
|
5
|
+
export default SheetBody;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { Dialog as DialogPrimitive } from 'bits-ui';
|
|
3
|
+
|
|
4
|
+
let {
|
|
5
|
+
ref = $bindable(null),
|
|
6
|
+
type = 'button',
|
|
7
|
+
...restProps
|
|
8
|
+
}: DialogPrimitive.CloseProps = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
<DialogPrimitive.Close bind:ref data-slot="sheet-close" {type} {...restProps} />
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
3
|
+
|
|
4
|
+
// A sheet stands on the hairline-and-ladder depth every other kit surface
|
|
5
|
+
// uses (README §Depth): an edge border against the dimmed page, no shadow.
|
|
6
|
+
// A side sheet takes a phone's whole width: a sliver of dimmed page beside
|
|
7
|
+
// it is too narrow to read and too narrow to aim at.
|
|
8
|
+
export const sheetVariants = tv({
|
|
9
|
+
base: 'bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out border-border fixed z-50 flex flex-col text-sm outline-none duration-200 ease-out data-closed:duration-150',
|
|
10
|
+
variants: {
|
|
11
|
+
side: {
|
|
12
|
+
right: 'data-closed:slide-out-to-right data-open:slide-in-from-right inset-y-0 right-0 h-full w-full border-l sm:max-w-sm',
|
|
13
|
+
left: 'data-closed:slide-out-to-left data-open:slide-in-from-left inset-y-0 left-0 h-full w-full border-r sm:max-w-sm',
|
|
14
|
+
bottom: 'data-closed:slide-out-to-bottom data-open:slide-in-from-bottom inset-x-0 bottom-0 max-h-[85%] rounded-t-xl border-t',
|
|
15
|
+
top: 'data-closed:slide-out-to-top data-open:slide-in-from-top inset-x-0 top-0 max-h-[85%] rounded-b-xl border-b'
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
defaultVariants: { side: 'right' }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type SheetSide = VariantProps<typeof sheetVariants>['side'];
|
|
22
|
+
</script>
|
|
23
|
+
|
|
24
|
+
<script lang="ts">
|
|
25
|
+
import { Dialog as SheetPrimitive } from 'bits-ui';
|
|
26
|
+
import type { ComponentProps, Snippet } from 'svelte';
|
|
27
|
+
import XIcon from '@lucide/svelte/icons/x';
|
|
28
|
+
import { cn, type WithoutChildrenOrChild } from '../../../utils.js';
|
|
29
|
+
import { Button } from '../button/index.js';
|
|
30
|
+
import SheetOverlay from './sheet-overlay.svelte';
|
|
31
|
+
import SheetPortal from './sheet-portal.svelte';
|
|
32
|
+
|
|
33
|
+
let {
|
|
34
|
+
ref = $bindable(null),
|
|
35
|
+
class: className,
|
|
36
|
+
side = 'right',
|
|
37
|
+
portalProps,
|
|
38
|
+
showCloseButton = true,
|
|
39
|
+
children,
|
|
40
|
+
...restProps
|
|
41
|
+
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
|
|
42
|
+
side?: SheetSide;
|
|
43
|
+
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
|
|
44
|
+
showCloseButton?: boolean;
|
|
45
|
+
children: Snippet;
|
|
46
|
+
} = $props();
|
|
47
|
+
</script>
|
|
48
|
+
|
|
49
|
+
<SheetPortal {...portalProps}>
|
|
50
|
+
<SheetOverlay />
|
|
51
|
+
<SheetPrimitive.Content
|
|
52
|
+
bind:ref
|
|
53
|
+
data-slot="sheet-content"
|
|
54
|
+
data-side={side}
|
|
55
|
+
class={cn(sheetVariants({ side }), className)}
|
|
56
|
+
{...restProps}
|
|
57
|
+
>
|
|
58
|
+
{@render children?.()}
|
|
59
|
+
{#if showCloseButton}
|
|
60
|
+
<SheetPrimitive.Close data-slot="sheet-close">
|
|
61
|
+
{#snippet child({ props })}
|
|
62
|
+
<Button variant="ghost" class="absolute top-3 right-3" size="icon-sm" {...props}>
|
|
63
|
+
<XIcon />
|
|
64
|
+
<span class="sr-only">Close</span>
|
|
65
|
+
</Button>
|
|
66
|
+
{/snippet}
|
|
67
|
+
</SheetPrimitive.Close>
|
|
68
|
+
{/if}
|
|
69
|
+
</SheetPrimitive.Content>
|
|
70
|
+
</SheetPortal>
|