@poodle64/ui 2026.10.6 → 2026.10.8

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 CHANGED
@@ -96,6 +96,7 @@ rather than rebuilding it:
96
96
  | `record-switcher` | The open record's whole name as the top bar's control, its menu finding, grouping and picturing every record, and the record's pages as tabs. |
97
97
  | `document-pane` | One document beside what it belongs to: titled, closable, its page fitted, with paging and zoom (a `PageCanvas` in a `Panel`). |
98
98
  | `detail-panel` | The entity-detail surface: header with icon/eyebrow/title/`StatusBadge`/close, scrollable body, footer of actions. |
99
+ | `about` | About content for a page or dialogue: `name`, `version`, optional `description`/`brand`/`links`, and explicitly selected non-sensitive `diagnostics`. Never pass settings or environment objects. `writeClipboard` accepts the host clipboard API directly; otherwise the browser API is used. Mount the shared Toaster once for copy success/failure. |
99
100
  | `settings-shell` | The settings destination: a grouped section list of `NavItem` rows beside the section you are reading, both scrolling independently from `md`, stacked below it. Render it inside `AppShell` with `padded={false}`. |
100
101
  | `context-column` | The persistent right-hand column. A list page's: a standing `StatList` plus an optional detail that flows in on select. A record page's (`title`, `children`): the one opened item, closable, scrolling inside itself, and its `documentPane` beside it once the page's row has room. Below `xl` (1280px) the same content opens from a floating trigger instead of disappearing. See [A record's page](#a-records-page). |
101
102
  | `app-dialog` | The dialogue frame: titled header, scrollable body, footer action bar, five sizes (`xs`…`xl`), and an `onOpenChange` for the dismissals the caller did not drive. |
@@ -0,0 +1,46 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { toast } from 'svelte-sonner';
4
+ import Panel from '../panel/panel.svelte';
5
+ import FactGrid from '../fact-grid/fact-grid.svelte';
6
+ import Button from '../button/button.svelte';
7
+
8
+ let { name, version, description, brand, links = [], diagnostics = {},
9
+ writeClipboard = (text: string) => navigator.clipboard.writeText(text) }: {
10
+ name: string;
11
+ version: string;
12
+ description?: string;
13
+ brand?: Snippet;
14
+ links?: readonly { label: string; href: string }[];
15
+ /** Explicit non-sensitive fields, never a settings/environment object. */
16
+ diagnostics?: Record<string, string>;
17
+ /** Pass the host's clipboard API directly where the browser API is unavailable. */
18
+ writeClipboard?: (text: string) => Promise<void>;
19
+ } = $props();
20
+ const facts = $derived([
21
+ { key: 'version', label: 'Version', value: version },
22
+ ...Object.entries(diagnostics).map(([label, value]) => ({ key: `diagnostic:${label}`, label, value }))
23
+ ]);
24
+
25
+ async function copyDiagnostics() {
26
+ try {
27
+ await writeClipboard(`${name}\n${facts.map((fact) => `${fact.label}: ${fact.value}`).join('\n')}`);
28
+ toast.success('Diagnostics copied');
29
+ } catch {
30
+ toast.error('Could not copy diagnostics', { description: 'Clipboard access is unavailable.' });
31
+ }
32
+ }
33
+ </script>
34
+
35
+ <Panel title={name} {description}>
36
+ <div class="flex flex-col gap-4">
37
+ {#if brand}{@render brand()}{/if}
38
+ <FactGrid {facts} />
39
+ <div class="flex flex-wrap gap-2">
40
+ {#each links as link}
41
+ <Button variant="outline" href={link.href} target="_blank" rel="noopener noreferrer">{link.label}</Button>
42
+ {/each}
43
+ <Button variant="outline" onclick={copyDiagnostics}>Copy diagnostics</Button>
44
+ </div>
45
+ </div>
46
+ </Panel>
@@ -0,0 +1,18 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ name: string;
4
+ version: string;
5
+ description?: string;
6
+ brand?: Snippet;
7
+ links?: readonly {
8
+ label: string;
9
+ href: string;
10
+ }[];
11
+ /** Explicit non-sensitive fields, never a settings/environment object. */
12
+ diagnostics?: Record<string, string>;
13
+ /** Pass the host's clipboard API directly where the browser API is unavailable. */
14
+ writeClipboard?: (text: string) => Promise<void>;
15
+ };
16
+ declare const About: import("svelte").Component<$$ComponentProps, {}, "">;
17
+ type About = ReturnType<typeof About>;
18
+ export default About;
@@ -0,0 +1,2 @@
1
+ export { default as About } from './about.svelte';
2
+ export { default } from './about.svelte';
@@ -0,0 +1,2 @@
1
+ export { default as About } from './about.svelte';
2
+ export { default } from './about.svelte';
@@ -427,6 +427,7 @@
427
427
 
428
428
  const money = (value: number | string) => ledgerMoney(value, currency, locale);
429
429
  const number = (n: number) => n.toLocaleString(locale);
430
+ const titleColumn = $derived(resolved.find((c) => c.key === 'title'));
430
431
  const count = (n: number) => `${number(n)} ${n === 1 ? noun[0] : noun[1]}`;
431
432
  const income = (value: number | string | null | undefined) => cents(value) > 0;
432
433
  /** Green for money in; a conversion is neither in nor out, so plain. */
@@ -500,6 +501,7 @@
500
501
  {#if c.key === 'date'}
501
502
  <span class={cn(CELL, through, 'text-muted-foreground self-center tabular-nums')} {style}>
502
503
  {ledgerDate(r.date, WIDE)}
504
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}
503
505
  </span>
504
506
  {:else if c.key === 'title'}
505
507
  <span
@@ -514,6 +516,7 @@
514
516
  <span class="min-w-0 truncate">{r.title}</span>
515
517
  {#if r.attachment}{@render clip(r)}{/if}
516
518
  {@render chip(r)}
519
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}
517
520
  </span>
518
521
  {#if r.note || r.origins}
519
522
  <span class="text-muted-foreground flex min-w-0 items-center gap-2 text-xs font-normal">
@@ -535,18 +538,22 @@
535
538
  data-slot="ledger-amount"
536
539
  >
537
540
  {money(r.amount)}
541
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}
538
542
  </span>
539
543
  {:else if c.key === 'balance'}
540
544
  <span class={cn(CELL, END, through, 'self-center tabular-nums')} {style}
541
- >{balanceText(r)}</span
545
+ >{balanceText(r)}
546
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}</span
542
547
  >
543
548
  {:else if c.cell}
544
549
  <span class={cn(CELL, through, 'flex items-center self-center')} {style}
545
- >{@render c.cell(r)}</span
550
+ >{@render c.cell(r)}
551
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}</span
546
552
  >
547
553
  {:else}
548
554
  <span class={cn(CELL, through, 'self-center', c.align === 'end' && END)} {style}>
549
555
  {c.text?.(r) ?? ''}
556
+ {#if c.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render c.actions(r)}</span>{/if}
550
557
  </span>
551
558
  {/if}
552
559
  {/each}
@@ -661,6 +668,7 @@
661
668
  <span class="min-w-0 truncate font-medium">{r.title}</span>
662
669
  {#if r.attachment}{@render clip(r)}{/if}
663
670
  {@render chip(r)}
671
+ {#if titleColumn?.actions}<span class="pointer-events-auto flex flex-none items-center gap-1" data-slot="ledger-actions">{@render titleColumn.actions(r)}</span>{/if}
664
672
  </span>
665
673
  {#if two || r.origins}
666
674
  <span class="text-muted-foreground flex min-w-0 items-center gap-2 text-xs">
@@ -88,7 +88,15 @@ export interface ListColumn<R> {
88
88
  * order; the balance is always last, and off until ticked. Every other key is
89
89
  * the module's, in its order.
90
90
  */
91
- export type LedgerColumn<R extends LedgerRow = LedgerRow> = ListColumn<R>;
91
+ export interface LedgerColumn<R extends LedgerRow = LedgerRow> extends ListColumn<R> {
92
+ /**
93
+ * Controls after the column's value in each row, the ledger's own cells
94
+ * included: a payee's "Show only / Hide". Drawn `pointer-events-auto`, so
95
+ * a click lands on a control and not on the row; on a phone, only the
96
+ * title's are drawn, after its chips.
97
+ */
98
+ actions?: Snippet<[R]>;
99
+ }
92
100
  /** The viewer's own choices, which the consumer persists. */
93
101
  export interface LedgerPreferences {
94
102
  /** The ticked columns' keys, `balance` included. */
@@ -7,11 +7,26 @@
7
7
  import InfoIcon from '@lucide/svelte/icons/info';
8
8
  import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
9
9
 
10
- let { ...restProps }: SonnerProps = $props();
10
+ let { toastOptions = {}, ...restProps }: SonnerProps = $props();
11
+ const options = $derived({
12
+ ...toastOptions,
13
+ style: 'font-family: var(--ds-font-body, inherit); font-size: var(--ds-text-sm, 0.875rem); ' + (toastOptions.style ?? ''),
14
+ classes: {
15
+ icon: 'size-4!',
16
+ content: 'min-w-0 max-h-[calc(100dvh-6rem)] overflow-y-auto',
17
+ title: 'whitespace-pre-wrap [overflow-wrap:anywhere]',
18
+ description: 'whitespace-pre-wrap [overflow-wrap:anywhere] text-inherit!',
19
+ ...toastOptions.classes
20
+ }
21
+ });
11
22
  </script>
12
23
 
13
24
  <Sonner
14
25
  theme={mode.current}
26
+ position="top-right"
27
+ closeButton
28
+ richColors={false}
29
+ toastOptions={options}
15
30
  class="toaster group"
16
31
  style="--normal-bg: var(--popover, var(--ds-color-surface-3)); --normal-text: var(--popover-foreground, var(--ds-color-foreground)); --normal-border: var(--border);"
17
32
  {...restProps}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/ui",
3
- "version": "2026.10.6",
3
+ "version": "2026.10.8",
4
4
  "description": "Household shared component layer: shadcn-svelte primitives (bits-ui) plus the composed page chrome every app builds its routes from, restyled by each app's @poodle64/design-tokens alias layer. One fix reaches every app.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.10.6",
4
+ "version": "2026.10.8",
5
5
  "generatedBy": "scripts/generate-registry.mjs",
6
6
  "source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
7
- "componentCount": 66,
7
+ "componentCount": 67,
8
8
  "situationCount": 18
9
9
  },
10
10
  "situations": [
@@ -404,6 +404,13 @@
404
404
  "import": "@poodle64/ui/settings-shell",
405
405
  "props": "sections, children, currentPath?, listLabel?",
406
406
  "insteadOf": "a hand-built two-pane settings layout, or a card of tab links in a grid, per app"
407
+ },
408
+ {
409
+ "name": "About",
410
+ "dir": "about",
411
+ "import": "@poodle64/ui/about",
412
+ "props": "name, version, description?, brand?, links?, diagnostics?, writeClipboard?",
413
+ "insteadOf": "a per-app About page or dialogue with duplicated version, links and diagnostic copying"
407
414
  }
408
415
  ]
409
416
  },
@@ -1,7 +1,7 @@
1
1
  # @poodle64/ui — situation → component map
2
2
 
3
3
  <!-- GENERATED by scripts/generate-registry.mjs from scripts/situations.json + package source. DO NOT EDIT. -->
4
- Generated from `@poodle64/ui@2026.10.6`. 66 components, 18 situations.
4
+ Generated from `@poodle64/ui@2026.10.8`. 67 components, 18 situations.
5
5
 
6
6
  **Read this before writing a `<div>`.** Find the SITUATION you are in below, then compose the component named for it — do not hand-build it from raw `Card` or utility classes. Import is `import { Name } from '<import path>'`. Props marked `?` are optional. This map is the retrieval step the [`frontend-design` skill] makes mandatory; the [CHI 2026 study] measured composing-from-a-registry at 95% design-system compliance against 71% for writing the CSS from a prose style guide.
7
7
 
@@ -160,6 +160,7 @@ _The one settings surface: a grouped section list beside the section you are rea
160
160
  | Component | Import | Key props | Reach for it instead of |
161
161
  | --- | --- | --- | --- |
162
162
  | `SettingsShell` | `@poodle64/ui/settings-shell` | `sections, children, currentPath?, listLabel?` | a hand-built two-pane settings layout, or a card of tab links in a grid, per app |
163
+ | `About` | `@poodle64/ui/about` | `name, version, description?, brand?, links?, diagnostics?, writeClipboard?` | a per-app About page or dialogue with duplicated version, links and diagnostic copying |
163
164
 
164
165
  ## A dialog or overlay
165
166