@poodle64/ui 2026.8.11 → 2026.8.13

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 (52) hide show
  1. package/README.md +164 -20
  2. package/dist/components/ui/collection-detail/collection-detail.svelte +169 -0
  3. package/dist/components/ui/collection-detail/collection-detail.svelte.d.ts +47 -0
  4. package/dist/components/ui/collection-detail/index.d.ts +3 -0
  5. package/dist/components/ui/collection-detail/index.js +2 -0
  6. package/dist/components/ui/document-detail/document-detail.svelte +184 -0
  7. package/dist/components/ui/document-detail/document-detail.svelte.d.ts +34 -0
  8. package/dist/components/ui/document-detail/index.d.ts +3 -0
  9. package/dist/components/ui/document-detail/index.js +2 -0
  10. package/dist/components/ui/library-browse/document-table.svelte +101 -0
  11. package/dist/components/ui/library-browse/document-table.svelte.d.ts +10 -0
  12. package/dist/components/ui/library-browse/facet-rail.svelte +62 -0
  13. package/dist/components/ui/library-browse/facet-rail.svelte.d.ts +8 -0
  14. package/dist/components/ui/library-browse/index.d.ts +3 -0
  15. package/dist/components/ui/library-browse/index.js +2 -0
  16. package/dist/components/ui/library-browse/library-browse.svelte +235 -0
  17. package/dist/components/ui/library-browse/library-browse.svelte.d.ts +44 -0
  18. package/dist/components/ui/library-browse/types.d.ts +101 -0
  19. package/dist/components/ui/library-browse/types.js +1 -0
  20. package/dist/components/ui/schema-form/context.d.ts +31 -0
  21. package/dist/components/ui/schema-form/context.js +10 -0
  22. package/dist/components/ui/schema-form/data.d.ts +42 -0
  23. package/dist/components/ui/schema-form/data.js +97 -0
  24. package/dist/components/ui/schema-form/dispatch.d.ts +35 -0
  25. package/dist/components/ui/schema-form/dispatch.js +139 -0
  26. package/dist/components/ui/schema-form/index.d.ts +5 -0
  27. package/dist/components/ui/schema-form/index.js +5 -0
  28. package/dist/components/ui/schema-form/schema-form-control.svelte +146 -0
  29. package/dist/components/ui/schema-form/schema-form-control.svelte.d.ts +7 -0
  30. package/dist/components/ui/schema-form/schema-form-element.svelte +105 -0
  31. package/dist/components/ui/schema-form/schema-form-element.svelte.d.ts +7 -0
  32. package/dist/components/ui/schema-form/schema-form-widget.svelte +169 -0
  33. package/dist/components/ui/schema-form/schema-form-widget.svelte.d.ts +18 -0
  34. package/dist/components/ui/schema-form/schema-form.svelte +153 -0
  35. package/dist/components/ui/schema-form/schema-form.svelte.d.ts +4 -0
  36. package/dist/components/ui/schema-form/types.d.ts +61 -0
  37. package/dist/components/ui/schema-form/types.js +21 -0
  38. package/dist/components/ui/schema-form/widgets/radio-field.svelte +48 -0
  39. package/dist/components/ui/schema-form/widgets/radio-field.svelte.d.ts +15 -0
  40. package/dist/components/ui/schema-form/widgets/slider-field.svelte +51 -0
  41. package/dist/components/ui/schema-form/widgets/slider-field.svelte.d.ts +13 -0
  42. package/dist/components/ui/schema-form/widgets/tags-field.svelte +98 -0
  43. package/dist/components/ui/schema-form/widgets/tags-field.svelte.d.ts +12 -0
  44. package/dist/components/ui/schema-form/widgets/unknown-field.svelte +123 -0
  45. package/dist/components/ui/schema-form/widgets/unknown-field.svelte.d.ts +20 -0
  46. package/dist/components/ui/search-results/index.d.ts +3 -0
  47. package/dist/components/ui/search-results/index.js +2 -0
  48. package/dist/components/ui/search-results/search-results.svelte +187 -0
  49. package/dist/components/ui/search-results/search-results.svelte.d.ts +44 -0
  50. package/package.json +2 -1
  51. package/registry/component-map.json +52 -3
  52. package/registry/component-map.md +20 -1
@@ -0,0 +1,123 @@
1
+ <script lang="ts">
2
+ import TriangleAlert from '@lucide/svelte/icons/triangle-alert';
3
+ import StatusBadge from '../../status-badge/status-badge.svelte';
4
+ import Input from '../../input/input.svelte';
5
+ import Label from '../../label/label.svelte';
6
+ import type { JsonSchema, UnknownReason } from '../types.js';
7
+ import { schemaType } from '../dispatch.js';
8
+
9
+ /**
10
+ * THE LOUD FALLBACK — the reason this package exists rather than a fourth
11
+ * hand-rolled renderer.
12
+ *
13
+ * The estate's outgoing renderers failed silently in three ways: an
14
+ * unrecognised `widget` hint rendered no input, a config group missing from a
15
+ * hardcoded order rendered nothing at all, and a nested hint was simply
16
+ * inert. Every one of those made the field VANISH, and a form with a missing
17
+ * field looks exactly like a form. Nobody noticed for months.
18
+ *
19
+ * So there is no silent branch anywhere in `<SchemaForm>`: everything the
20
+ * renderer cannot dispatch lands here, and here always renders. It says what
21
+ * it could not do, names the pointer so the fix is a copy-paste, shows the
22
+ * value that would otherwise have been lost, and keeps it editable whenever
23
+ * editing a raw value is safe — which is when the value is a primitive. An
24
+ * object or an array is shown, not edited: a text box over structured data
25
+ * is a data-loss affordance, not a fallback.
26
+ */
27
+ let {
28
+ label,
29
+ scope,
30
+ path,
31
+ reason,
32
+ detail,
33
+ value,
34
+ schema,
35
+ disabled = false,
36
+ id,
37
+ onchange
38
+ }: {
39
+ label: string;
40
+ /** JSON Pointer this control addressed, or the property's own pointer. */
41
+ scope: string;
42
+ /** Dot path into the value. */
43
+ path: string;
44
+ reason: UnknownReason;
45
+ /** What specifically could not be done, in one sentence. */
46
+ detail: string;
47
+ value: unknown;
48
+ schema?: JsonSchema;
49
+ disabled?: boolean;
50
+ id: string;
51
+ /** Emitted when the raw editor commits a primitive. */
52
+ onchange?: (next: unknown) => void;
53
+ } = $props();
54
+
55
+ // Editing is offered only where a text box can round-trip the value without
56
+ // destroying structure: a primitive, or an absent value under a primitive
57
+ // schema. Anything structured is shown read-only.
58
+ const declared = $derived(schemaType(schema));
59
+ const structural = $derived(
60
+ (value !== null && typeof value === 'object') || declared === 'object' || declared === 'array'
61
+ );
62
+ const editable = $derived(!structural);
63
+
64
+ const raw = $derived(
65
+ value === undefined || value === null
66
+ ? ''
67
+ : typeof value === 'object'
68
+ ? JSON.stringify(value, null, 2)
69
+ : String(value)
70
+ );
71
+
72
+ /** Coerce the typed text back to the schema's declared type where it can. */
73
+ function commit(text: string) {
74
+ if (declared === 'number' || declared === 'integer') {
75
+ const parsed = text.trim() === '' ? undefined : Number(text);
76
+ onchange?.(parsed !== undefined && Number.isFinite(parsed) ? parsed : text);
77
+ return;
78
+ }
79
+ if (declared === 'boolean') {
80
+ if (text === 'true' || text === 'false') return onchange?.(text === 'true');
81
+ }
82
+ onchange?.(text);
83
+ }
84
+ </script>
85
+
86
+ <div
87
+ role="group"
88
+ aria-label="Unrecognised field: {label}"
89
+ data-schema-form-unknown=""
90
+ data-unknown-reason={reason}
91
+ data-path={path}
92
+ class="border-status-warning/50 bg-status-warning/5 grid gap-2 rounded-lg border border-dashed p-3"
93
+ >
94
+ <div class="flex flex-wrap items-center gap-2">
95
+ <TriangleAlert class="text-status-warning size-4 shrink-0" aria-hidden="true" />
96
+ <span class="text-body font-semibold">{label}</span>
97
+ <StatusBadge status="warning" label="Unrecognised control" />
98
+ </div>
99
+
100
+ <p class="text-muted-foreground text-xs">
101
+ {detail}. Rendered raw and unstyled so the value is not lost, at
102
+ <code class="font-mono">{scope}</code>
103
+ </p>
104
+
105
+ {#if editable}
106
+ <Label for={id} class="text-2xs text-muted-foreground uppercase">Raw value</Label>
107
+ <Input
108
+ {id}
109
+ {disabled}
110
+ value={raw}
111
+ class="font-mono"
112
+ oninput={(event) => commit((event.currentTarget as HTMLInputElement).value)}
113
+ />
114
+ {:else}
115
+ <pre
116
+ data-schema-form-unknown-readonly=""
117
+ class="bg-surface-1 border-border text-muted-foreground max-h-48 overflow-auto rounded-md border p-2 font-mono text-xs">{raw ||
118
+ '(no value)'}</pre>
119
+ <p class="text-muted-foreground text-xs">
120
+ Structured value: shown, not edited, so nothing is silently rewritten.
121
+ </p>
122
+ {/if}
123
+ </div>
@@ -0,0 +1,20 @@
1
+ import type { JsonSchema, UnknownReason } from '../types.js';
2
+ type $$ComponentProps = {
3
+ label: string;
4
+ /** JSON Pointer this control addressed, or the property's own pointer. */
5
+ scope: string;
6
+ /** Dot path into the value. */
7
+ path: string;
8
+ reason: UnknownReason;
9
+ /** What specifically could not be done, in one sentence. */
10
+ detail: string;
11
+ value: unknown;
12
+ schema?: JsonSchema;
13
+ disabled?: boolean;
14
+ id: string;
15
+ /** Emitted when the raw editor commits a primitive. */
16
+ onchange?: (next: unknown) => void;
17
+ };
18
+ declare const UnknownField: import("svelte").Component<$$ComponentProps, {}, "">;
19
+ type UnknownField = ReturnType<typeof UnknownField>;
20
+ export default UnknownField;
@@ -0,0 +1,3 @@
1
+ export { default as SearchResults } from './search-results.svelte';
2
+ export { default } from './search-results.svelte';
3
+ export type { LibraryBadge, LibrarySearchResult, SearchSnippetSegment } from '../library-browse/types.js';
@@ -0,0 +1,2 @@
1
+ export { default as SearchResults } from './search-results.svelte';
2
+ export { default } from './search-results.svelte';
@@ -0,0 +1,187 @@
1
+ <script lang="ts">
2
+ /**
3
+ * SearchResults — a ranked retrieval answer: for each hit, the document's
4
+ * title, the passage that matched (with the matching spans emphasised), a
5
+ * source chip, consumer-mapped state, and a relevance figure.
6
+ *
7
+ * Designed, not extracted (#30): no in-repo view existed to generalise
8
+ * from. The shape follows the documentation-search results pattern — title
9
+ * over highlighted snippet over source — rendered in this package's own
10
+ * vocabulary: the highlight is a tint of the app's accent so every
11
+ * consumer's results wear its own palette; the score is a mono, tabular
12
+ * figure per the machine-value rule; state chips are StatusBadge.
13
+ *
14
+ * Fixed props over plain data: the page owns the query and the fetch, and
15
+ * this only renders the answer. `<ol>` because rank IS the meaning.
16
+ */
17
+ import type { HTMLAttributes } from 'svelte/elements';
18
+ import { cn, type WithElementRef } from '../../../utils.js';
19
+ import SearchIcon from '@lucide/svelte/icons/search';
20
+ import Button from '../button/button.svelte';
21
+ import StatusBadge from '../status-badge/status-badge.svelte';
22
+ import EmptyState from '../empty-state/empty-state.svelte';
23
+ import ErrorState from '../error-state/error-state.svelte';
24
+ import LoadingState from '../loading-state/loading-state.svelte';
25
+ import type { LibrarySearchResult, SearchSnippetSegment } from '../library-browse/types.js';
26
+
27
+ let {
28
+ results,
29
+ query = '',
30
+ total,
31
+ loading = false,
32
+ error = null,
33
+ promptTitle = 'Search the catalogue',
34
+ promptDescription = 'Results appear here as you search.',
35
+ noResultsTitle,
36
+ noResultsDescription = 'Try a broader term, or search a different collection.',
37
+ resultHref,
38
+ onOpen,
39
+ onRetry,
40
+ ref = $bindable(null),
41
+ class: className,
42
+ ...restProps
43
+ }: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, 'results'>> & {
44
+ /** `Omit` above: svelte/elements declares a legacy `results` HTML
45
+ * attribute (number) on every element, and without the omission this
46
+ * prop's type silently intersects with it — callers then cannot pass
47
+ * an array at all. Caught by CI's svelte-check on the test call sites. */
48
+ results: LibrarySearchResult[];
49
+ /** The term the page searched for; shown in the count line and empty copy. */
50
+ query?: string;
51
+ /** Total hits when the page truncates; defaults to the rows given. */
52
+ total?: number;
53
+ loading?: boolean;
54
+ error?: string | null;
55
+ /** Copy for the surface before any search has been made. */
56
+ promptTitle?: string;
57
+ promptDescription?: string;
58
+ /** Copy when a search matched nothing; defaults name the query. */
59
+ noResultsTitle?: string;
60
+ noResultsDescription?: string;
61
+ /** The app's own routed link per hit; this package has no router. */
62
+ resultHref?: (result: LibrarySearchResult) => string;
63
+ onOpen?: (result: LibrarySearchResult) => void;
64
+ onRetry?: () => void;
65
+ } = $props();
66
+
67
+ const shownTotal = $derived(total ?? results.length);
68
+
69
+ /** Normalise a snippet: plain text becomes one unhighlighted segment. */
70
+ function segments(snippet: string | SearchSnippetSegment[]): SearchSnippetSegment[] {
71
+ return typeof snippet === 'string' ? [{ text: snippet }] : snippet;
72
+ }
73
+
74
+ /** Relevance as a whole percentage; the wire value is [0, 1]. */
75
+ function pct(score: number): number {
76
+ return Math.round(Math.min(1, Math.max(0, score)) * 100);
77
+ }
78
+ </script>
79
+
80
+ <div bind:this={ref} class={cn('flex flex-col gap-3', className)} {...restProps}>
81
+ {#if loading}
82
+ <LoadingState message="Searching…" />
83
+ {:else if error}
84
+ <ErrorState message={error}>
85
+ {#snippet action()}
86
+ {#if onRetry}
87
+ <Button variant="outline" onclick={onRetry}>Retry</Button>
88
+ {/if}
89
+ {/snippet}
90
+ </ErrorState>
91
+ {:else if results.length === 0}
92
+ {#if query}
93
+ <EmptyState
94
+ icon={SearchIcon}
95
+ title={noResultsTitle ?? `No results for “${query}”`}
96
+ description={noResultsDescription}
97
+ />
98
+ {:else}
99
+ <EmptyState icon={SearchIcon} title={promptTitle} description={promptDescription} />
100
+ {/if}
101
+ {:else}
102
+ <p class="text-muted-foreground text-sm">
103
+ <span class="font-mono tabular-nums">{shownTotal}</span>
104
+ result{shownTotal === 1 ? '' : 's'}{query ? ` for “${query}”` : ''}
105
+ </p>
106
+
107
+ <ol class="divide-border border-border ds-edge bg-card divide-y rounded-lg border">
108
+ {#each results as result (result.id)}
109
+ <li>
110
+ <article class="flex items-start gap-4 px-4 py-3.5">
111
+ <div class="min-w-0 flex-1">
112
+ {#if resultHref}
113
+ <a
114
+ href={resultHref(result)}
115
+ class="font-medium hover:underline"
116
+ onclick={() => onOpen?.(result)}
117
+ >
118
+ {result.title}
119
+ </a>
120
+ {:else if onOpen}
121
+ <button
122
+ type="button"
123
+ class="text-left font-medium hover:underline"
124
+ onclick={() => onOpen(result)}
125
+ >
126
+ {result.title}
127
+ </button>
128
+ {:else}
129
+ <span class="font-medium">{result.title}</span>
130
+ {/if}
131
+
132
+ {#if result.source || result.meta}
133
+ <div class="text-muted-foreground mt-0.5 flex flex-wrap items-center gap-2 text-xs">
134
+ {#if result.source}
135
+ <span class="bg-surface-2 rounded px-1.5 py-0.5 font-medium">
136
+ {result.source}
137
+ </span>
138
+ {/if}
139
+ {#if result.meta}
140
+ <span>{result.meta}</span>
141
+ {/if}
142
+ </div>
143
+ {/if}
144
+
145
+ {#if result.snippet}
146
+ <!-- The matched passage. <mark> carries the semantics; the UA's
147
+ black-on-yellow default is replaced with a tint of the
148
+ app's own accent, so the highlight is skinned by the
149
+ consumer's palette like everything else. -->
150
+ <p class="text-muted-foreground mt-1.5 line-clamp-3 text-sm">
151
+ {#each segments(result.snippet) as segment, i (i)}
152
+ {#if segment.highlight}
153
+ <mark class="bg-primary/15 text-foreground rounded-sm px-0.5 font-medium">
154
+ {segment.text}
155
+ </mark>
156
+ {:else}
157
+ {segment.text}
158
+ {/if}
159
+ {/each}
160
+ </p>
161
+ {/if}
162
+
163
+ {#if result.badges?.length}
164
+ <div class="mt-1.5 flex flex-wrap gap-1">
165
+ <!-- Keyed by index: LibraryBadge carries no id, and two states
166
+ can legitimately share a label. -->
167
+ {#each result.badges as badge, i (i)}
168
+ <StatusBadge status={badge.status} label={badge.label} />
169
+ {/each}
170
+ </div>
171
+ {/if}
172
+ </div>
173
+
174
+ {#if result.score !== undefined}
175
+ <span
176
+ class="text-muted-foreground mt-0.5 flex-none font-mono text-xs tabular-nums"
177
+ aria-label="Relevance {pct(result.score)} percent"
178
+ >
179
+ {pct(result.score)}%
180
+ </span>
181
+ {/if}
182
+ </article>
183
+ </li>
184
+ {/each}
185
+ </ol>
186
+ {/if}
187
+ </div>
@@ -0,0 +1,44 @@
1
+ /**
2
+ * SearchResults — a ranked retrieval answer: for each hit, the document's
3
+ * title, the passage that matched (with the matching spans emphasised), a
4
+ * source chip, consumer-mapped state, and a relevance figure.
5
+ *
6
+ * Designed, not extracted (#30): no in-repo view existed to generalise
7
+ * from. The shape follows the documentation-search results pattern — title
8
+ * over highlighted snippet over source — rendered in this package's own
9
+ * vocabulary: the highlight is a tint of the app's accent so every
10
+ * consumer's results wear its own palette; the score is a mono, tabular
11
+ * figure per the machine-value rule; state chips are StatusBadge.
12
+ *
13
+ * Fixed props over plain data: the page owns the query and the fetch, and
14
+ * this only renders the answer. `<ol>` because rank IS the meaning.
15
+ */
16
+ import type { HTMLAttributes } from 'svelte/elements';
17
+ import { type WithElementRef } from '../../../utils.js';
18
+ import type { LibrarySearchResult } from '../library-browse/types.js';
19
+ type $$ComponentProps = WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, 'results'>> & {
20
+ /** `Omit` above: svelte/elements declares a legacy `results` HTML
21
+ * attribute (number) on every element, and without the omission this
22
+ * prop's type silently intersects with it — callers then cannot pass
23
+ * an array at all. Caught by CI's svelte-check on the test call sites. */
24
+ results: LibrarySearchResult[];
25
+ /** The term the page searched for; shown in the count line and empty copy. */
26
+ query?: string;
27
+ /** Total hits when the page truncates; defaults to the rows given. */
28
+ total?: number;
29
+ loading?: boolean;
30
+ error?: string | null;
31
+ /** Copy for the surface before any search has been made. */
32
+ promptTitle?: string;
33
+ promptDescription?: string;
34
+ /** Copy when a search matched nothing; defaults name the query. */
35
+ noResultsTitle?: string;
36
+ noResultsDescription?: string;
37
+ /** The app's own routed link per hit; this package has no router. */
38
+ resultHref?: (result: LibrarySearchResult) => string;
39
+ onOpen?: (result: LibrarySearchResult) => void;
40
+ onRetry?: () => void;
41
+ };
42
+ declare const SearchResults: import("svelte").Component<$$ComponentProps, {}, "ref">;
43
+ type SearchResults = ReturnType<typeof SearchResults>;
44
+ export default SearchResults;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@poodle64/ui",
3
- "version": "2026.8.11",
3
+ "version": "2026.8.13",
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",
@@ -40,6 +40,7 @@
40
40
  }
41
41
  },
42
42
  "dependencies": {
43
+ "@jsonforms/core": "^3.8.0",
43
44
  "@lucide/svelte": "^1.25.0",
44
45
  "@tanstack/table-core": "^8.21.3",
45
46
  "clsx": "^2.1.1",
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.8.11",
4
+ "version": "2026.8.13",
5
5
  "generatedBy": "scripts/generate-registry.mjs",
6
6
  "source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
7
- "componentCount": 49,
8
- "situationCount": 12
7
+ "componentCount": 54,
8
+ "situationCount": 14
9
9
  },
10
10
  "situations": [
11
11
  {
@@ -155,6 +155,55 @@
155
155
  }
156
156
  ]
157
157
  },
158
+ {
159
+ "key": "server-described-form",
160
+ "title": "A form the server described",
161
+ "description": "A config object whose shape arrives at runtime as a JSON Schema plus a JSON Forms UI Schema — the renderer walks both and dispatches to this package's widgets. Anything it cannot dispatch renders flagged, never blank.",
162
+ "components": [
163
+ {
164
+ "name": "SchemaForm",
165
+ "dir": "schema-form",
166
+ "import": "@poodle64/ui/schema-form",
167
+ "props": "schema, value, onChange, uischema?, disabled?, idPrefix?",
168
+ "insteadOf": "a hand-written renderer over a home-grown hint vocabulary, which is how a field goes missing in silence"
169
+ }
170
+ ]
171
+ },
172
+ {
173
+ "key": "library-data",
174
+ "title": "Library data the app fetched",
175
+ "description": "The estate's shared document-library surfaces — a faceted catalogue, one collection's detail, one document's detail, a ranked search answer — rendered from plain props the app maps its own API responses into. The package never fetches and never routes: the app owns the query, the offset and every link. Configuration is the schema-driven half (SchemaForm); library data is fixed components because its shape is stable.",
176
+ "components": [
177
+ {
178
+ "name": "LibraryBrowse",
179
+ "dir": "library-browse",
180
+ "import": "@poodle64/ui/library-browse",
181
+ "props": "documents, facets?, query?, total?, offset?, limit?, loading?, error?, onQueryChange?, onFacetChange?, onPageChange?, documentHref?, onOpenDocument?, toolbar?",
182
+ "insteadOf": "a hand-built catalogue page: search box, facet rail, filter chips, results table and pager re-derived per app"
183
+ },
184
+ {
185
+ "name": "CollectionDetail",
186
+ "dir": "collection-detail",
187
+ "import": "@poodle64/ui/collection-detail",
188
+ "props": "collection, stats?, documents?, documentsTotal?, loading?, error?, documentsLoading?, documentsError?, onPageChange?, documentHref?, onOpenDocument?, actions?, children?",
189
+ "insteadOf": "a bespoke collection page assembled from raw Card, a hand-built table and its own stat rows"
190
+ },
191
+ {
192
+ "name": "DocumentDetail",
193
+ "dir": "document-detail",
194
+ "import": "@poodle64/ui/document-detail",
195
+ "props": "document, loading?, error?, collectionHref?, onOpenCollection?, actions?, children?",
196
+ "insteadOf": "a bespoke document page: identity dl, locations, tags and memberships hand-laid-out per app"
197
+ },
198
+ {
199
+ "name": "SearchResults",
200
+ "dir": "search-results",
201
+ "import": "@poodle64/ui/search-results",
202
+ "props": "results, query?, total?, loading?, error?, resultHref?, onOpen?, onRetry?",
203
+ "insteadOf": "a hand-built ranked hit list minting its own highlight and relevance styling"
204
+ }
205
+ ]
206
+ },
158
207
  {
159
208
  "key": "page-frame",
160
209
  "title": "The frame every route shares",
@@ -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.8.11`. 49 components, 12 situations.
4
+ Generated from `@poodle64/ui@2026.8.13`. 54 components, 14 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
 
@@ -83,6 +83,25 @@ _The generic titled card every route reaches for — an optional header with ico
83
83
  | --- | --- | --- | --- |
84
84
  | `Panel` | `@poodle64/ui/panel` | `children, title?, subtitle?, icon?, action?, pad?` | raw Card + CardHeader + CardTitle assembled by hand on every route |
85
85
 
86
+ ## A form the server described
87
+
88
+ _A config object whose shape arrives at runtime as a JSON Schema plus a JSON Forms UI Schema — the renderer walks both and dispatches to this package's widgets. Anything it cannot dispatch renders flagged, never blank._
89
+
90
+ | Component | Import | Key props | Reach for it instead of |
91
+ | --- | --- | --- | --- |
92
+ | `SchemaForm` | `@poodle64/ui/schema-form` | `schema, value, onChange, uischema?, disabled?, idPrefix?` | a hand-written renderer over a home-grown hint vocabulary, which is how a field goes missing in silence |
93
+
94
+ ## Library data the app fetched
95
+
96
+ _The estate's shared document-library surfaces — a faceted catalogue, one collection's detail, one document's detail, a ranked search answer — rendered from plain props the app maps its own API responses into. The package never fetches and never routes: the app owns the query, the offset and every link. Configuration is the schema-driven half (SchemaForm); library data is fixed components because its shape is stable._
97
+
98
+ | Component | Import | Key props | Reach for it instead of |
99
+ | --- | --- | --- | --- |
100
+ | `LibraryBrowse` | `@poodle64/ui/library-browse` | `documents, facets?, query?, total?, offset?, limit?, loading?, error?, onQueryChange?, onFacetChange?, onPageChange?, documentHref?, onOpenDocument?, toolbar?` | a hand-built catalogue page: search box, facet rail, filter chips, results table and pager re-derived per app |
101
+ | `CollectionDetail` | `@poodle64/ui/collection-detail` | `collection, stats?, documents?, documentsTotal?, loading?, error?, documentsLoading?, documentsError?, onPageChange?, documentHref?, onOpenDocument?, actions?, children?` | a bespoke collection page assembled from raw Card, a hand-built table and its own stat rows |
102
+ | `DocumentDetail` | `@poodle64/ui/document-detail` | `document, loading?, error?, collectionHref?, onOpenCollection?, actions?, children?` | a bespoke document page: identity dl, locations, tags and memberships hand-laid-out per app |
103
+ | `SearchResults` | `@poodle64/ui/search-results` | `results, query?, total?, loading?, error?, resultHref?, onOpen?, onRetry?` | a hand-built ranked hit list minting its own highlight and relevance styling |
104
+
86
105
  ## The frame every route shares
87
106
 
88
107
  _The standing chrome — the app shell and its nav, the one page-title treatment, the persistent right-hand context column._