@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.
Files changed (62) hide show
  1. package/README.md +101 -11
  2. package/dist/components/feedback/report-widget.svelte +4 -1
  3. package/dist/components/feedback/report-widget.svelte.d.ts +1 -0
  4. package/dist/components/ui/app-identity/app-identity.svelte +7 -2
  5. package/dist/components/ui/app-shell/texture.d.ts +5 -5
  6. package/dist/components/ui/app-shell/texture.js +3 -3
  7. package/dist/components/ui/breadcrumbs/breadcrumbs.svelte +70 -0
  8. package/dist/components/ui/breadcrumbs/breadcrumbs.svelte.d.ts +32 -0
  9. package/dist/components/ui/breadcrumbs/index.d.ts +2 -0
  10. package/dist/components/ui/breadcrumbs/index.js +2 -0
  11. package/dist/components/ui/content-skeleton/content-skeleton.svelte +94 -0
  12. package/dist/components/ui/content-skeleton/content-skeleton.svelte.d.ts +25 -0
  13. package/dist/components/ui/content-skeleton/index.d.ts +2 -0
  14. package/dist/components/ui/content-skeleton/index.js +2 -0
  15. package/dist/components/ui/document-reader/document-reader.svelte +336 -0
  16. package/dist/components/ui/document-reader/document-reader.svelte.d.ts +65 -0
  17. package/dist/components/ui/document-reader/index.d.ts +2 -0
  18. package/dist/components/ui/document-reader/index.js +2 -0
  19. package/dist/components/ui/graph-canvas/graph-canvas.svelte +69 -2
  20. package/dist/components/ui/graph-canvas/graph-canvas.svelte.d.ts +2 -0
  21. package/dist/components/ui/graph-canvas/scene.d.ts +6 -0
  22. package/dist/components/ui/graph-canvas/scene.js +52 -21
  23. package/dist/components/ui/graph-canvas/types.d.ts +3 -0
  24. package/dist/components/ui/page-container/index.d.ts +2 -0
  25. package/dist/components/ui/page-container/index.js +2 -0
  26. package/dist/components/ui/page-container/page-container.svelte +39 -0
  27. package/dist/components/ui/page-container/page-container.svelte.d.ts +24 -0
  28. package/dist/components/ui/page-header/page-header.svelte +12 -7
  29. package/dist/components/ui/page-header/page-header.svelte.d.ts +3 -2
  30. package/dist/components/ui/page-section/index.d.ts +2 -0
  31. package/dist/components/ui/page-section/index.js +2 -0
  32. package/dist/components/ui/page-section/page-section.svelte +92 -0
  33. package/dist/components/ui/page-section/page-section.svelte.d.ts +36 -0
  34. package/dist/components/ui/sheet/index.d.ts +13 -0
  35. package/dist/components/ui/sheet/index.js +15 -0
  36. package/dist/components/ui/sheet/sheet-body.svelte +21 -0
  37. package/dist/components/ui/sheet/sheet-body.svelte.d.ts +5 -0
  38. package/dist/components/ui/sheet/sheet-close.svelte +11 -0
  39. package/dist/components/ui/sheet/sheet-close.svelte.d.ts +4 -0
  40. package/dist/components/ui/sheet/sheet-content.svelte +70 -0
  41. package/dist/components/ui/sheet/sheet-content.svelte.d.ts +37 -0
  42. package/dist/components/ui/sheet/sheet-description.svelte +20 -0
  43. package/dist/components/ui/sheet/sheet-description.svelte.d.ts +4 -0
  44. package/dist/components/ui/sheet/sheet-footer.svelte +23 -0
  45. package/dist/components/ui/sheet/sheet-footer.svelte.d.ts +5 -0
  46. package/dist/components/ui/sheet/sheet-header.svelte +21 -0
  47. package/dist/components/ui/sheet/sheet-header.svelte.d.ts +5 -0
  48. package/dist/components/ui/sheet/sheet-overlay.svelte +20 -0
  49. package/dist/components/ui/sheet/sheet-overlay.svelte.d.ts +4 -0
  50. package/dist/components/ui/sheet/sheet-portal.svelte +7 -0
  51. package/dist/components/ui/sheet/sheet-portal.svelte.d.ts +3 -0
  52. package/dist/components/ui/sheet/sheet-title.svelte +21 -0
  53. package/dist/components/ui/sheet/sheet-title.svelte.d.ts +4 -0
  54. package/dist/components/ui/sheet/sheet-trigger.svelte +11 -0
  55. package/dist/components/ui/sheet/sheet-trigger.svelte.d.ts +4 -0
  56. package/dist/components/ui/sheet/sheet.svelte +7 -0
  57. package/dist/components/ui/sheet/sheet.svelte.d.ts +3 -0
  58. package/dist/components/ui/skeleton/skeleton.svelte +1 -1
  59. package/dist/styles.css +22 -0
  60. package/package.json +2 -1
  61. package/registry/component-map.json +43 -4
  62. 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
- const focus = this.hovered ?? chosen;
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 lit = (s) => !focus || s === focus || !!near?.has(s);
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 || !!focus);
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
- let path = rest;
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 || !focus))
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 = focus ? 0.07 : Math.min(0.65, 0.12 + 0.35 * k);
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 && !focus)
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
- if (!s.node.emphasis && !ring && !this.matches.has(s))
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
- order.push(...this.ranked);
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,2 @@
1
+ export { default as PageContainer } from './page-container.svelte';
2
+ export { default } from './page-container.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as PageContainer } from './page-container.svelte';
2
+ export { default } from './page-container.svelte';
@@ -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 itself stays the app's, as a snippet: a breadcrumb trail is made
27
- * of routed links, and a package with no SvelteKit runtime of its own cannot
28
- * own those (the same reasoning that makes AppShell take `currentPath` as a
29
- * prop rather than importing `$app/state`).
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 — the app's own routed links. */
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 — the app's own routed links. */
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,2 @@
1
+ export { default as PageSection } from './page-section.svelte';
2
+ export { default } from './page-section.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as PageSection } from './page-section.svelte';
2
+ export { default } from './page-section.svelte';
@@ -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,4 @@
1
+ import { Dialog as DialogPrimitive } from 'bits-ui';
2
+ declare const SheetClose: import("svelte").Component<DialogPrimitive.TriggerProps, {}, "ref">;
3
+ type SheetClose = ReturnType<typeof SheetClose>;
4
+ export default SheetClose;
@@ -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>