@poodle64/ui 2026.8.13 → 2026.8.15
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 +5 -1
- package/dist/components/ui/document-detail/document-detail.svelte +21 -8
- package/dist/components/ui/library-browse/document-table.svelte +40 -9
- package/dist/components/ui/library-browse/library-browse.svelte +24 -1
- package/package.json +1 -1
- package/registry/component-map.json +1 -1
- package/registry/component-map.md +1 -1
package/README.md
CHANGED
|
@@ -751,7 +751,11 @@ node harness/additivity.mjs ui-v2026.8.3
|
|
|
751
751
|
Tailwind chain, exactly as a consuming app wires it — a class that generates no
|
|
752
752
|
rule, a colour that resolves to nothing, a registered theme key nothing reads, a
|
|
753
753
|
variant whose selector cannot match the DOM, and a sizing utility that has
|
|
754
|
-
silently changed meaning all fail there rather than shipping.
|
|
754
|
+
silently changed meaning all fail there rather than shipping. It also sweeps the
|
|
755
|
+
source for the one craft defect none of those can see — a content container that
|
|
756
|
+
draws a border and declares no surface, so it computes transparent and the page
|
|
757
|
+
shows through it (`src/test/bordered-surface.test.ts`, which carries its own
|
|
758
|
+
limits at the foot of the file).
|
|
755
759
|
|
|
756
760
|
`pnpm run test:browser` is the leg nothing else can stand in for: jsdom applies
|
|
757
761
|
no stylesheet and returns unresolved `var(…)` literals, so it passes just as
|
|
@@ -69,17 +69,28 @@
|
|
|
69
69
|
<EmptyState title={notFoundTitle} description={notFoundDescription} />
|
|
70
70
|
{:else}
|
|
71
71
|
<DetailPanel eyebrow="Document" title={document.title} titleFace="display" footer={actions}>
|
|
72
|
-
|
|
72
|
+
<!-- min-w-0 on the grid AND on every cell and value below: a grid or
|
|
73
|
+
flex item defaults to min-width:auto, so it grows to its widest
|
|
74
|
+
child and `truncate` can never shrink it. A 64-character content
|
|
75
|
+
hash and a filesystem path therefore took this card to 1,124px
|
|
76
|
+
inside a 390px screen and pushed every VALUE off the side, leaving
|
|
77
|
+
the labels alone on it. -->
|
|
78
|
+
<div class="grid min-w-0 gap-4 sm:grid-cols-2">
|
|
73
79
|
{#if document.fields?.length}
|
|
74
|
-
<div>
|
|
80
|
+
<div class="min-w-0">
|
|
75
81
|
<h3 class="text-muted-foreground text-2xs mb-2 font-semibold tracking-wide uppercase">
|
|
76
82
|
Details
|
|
77
83
|
</h3>
|
|
78
84
|
<dl class="space-y-1 text-sm">
|
|
79
85
|
{#each document.fields as field (field.label)}
|
|
80
86
|
<div class="flex justify-between gap-4">
|
|
81
|
-
<dt class="text-muted-foreground">{field.label}</dt>
|
|
82
|
-
<dd
|
|
87
|
+
<dt class="text-muted-foreground shrink-0">{field.label}</dt>
|
|
88
|
+
<dd
|
|
89
|
+
class={field.mono
|
|
90
|
+
? 'min-w-0 truncate font-mono text-xs'
|
|
91
|
+
: 'min-w-0 truncate text-right'}
|
|
92
|
+
title={field.value}
|
|
93
|
+
>
|
|
83
94
|
{field.value}
|
|
84
95
|
</dd>
|
|
85
96
|
</div>
|
|
@@ -89,14 +100,16 @@
|
|
|
89
100
|
{/if}
|
|
90
101
|
|
|
91
102
|
{#if document.locations?.length}
|
|
92
|
-
<div>
|
|
103
|
+
<div class="min-w-0">
|
|
93
104
|
<h3 class="text-muted-foreground text-2xs mb-2 font-semibold tracking-wide uppercase">
|
|
94
105
|
Locations
|
|
95
106
|
</h3>
|
|
96
107
|
<ul class="space-y-1 text-sm">
|
|
97
108
|
{#each document.locations as location (location.path)}
|
|
98
109
|
<li class="flex items-center justify-between gap-2">
|
|
99
|
-
<span class="truncate font-mono text-xs"
|
|
110
|
+
<span class="min-w-0 truncate font-mono text-xs" title={location.path}
|
|
111
|
+
>{location.path}</span
|
|
112
|
+
>
|
|
100
113
|
<div class="flex shrink-0 items-center gap-1">
|
|
101
114
|
{#if location.primary}
|
|
102
115
|
<Badge variant="secondary">primary</Badge>
|
|
@@ -112,7 +125,7 @@
|
|
|
112
125
|
{/if}
|
|
113
126
|
|
|
114
127
|
{#if document.tags?.length}
|
|
115
|
-
<div>
|
|
128
|
+
<div class="min-w-0">
|
|
116
129
|
<h3 class="text-muted-foreground text-2xs mb-2 font-semibold tracking-wide uppercase">
|
|
117
130
|
Tags
|
|
118
131
|
</h3>
|
|
@@ -125,7 +138,7 @@
|
|
|
125
138
|
{/if}
|
|
126
139
|
|
|
127
140
|
{#if document.memberships?.length}
|
|
128
|
-
<div>
|
|
141
|
+
<div class="min-w-0">
|
|
129
142
|
<h3 class="text-muted-foreground text-2xs mb-2 font-semibold tracking-wide uppercase">
|
|
130
143
|
Collections
|
|
131
144
|
</h3>
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
// a consumer that passes no `collections` never sees an empty column —
|
|
5
5
|
// which is what lets one table serve both the whole-catalogue view and a
|
|
6
6
|
// single collection's slice.
|
|
7
|
+
//
|
|
8
|
+
// Columns also come and go with the VIEWPORT. The title is the only column
|
|
9
|
+
// a phone has room for, so the secondary ones drop by breakpoint rather
|
|
10
|
+
// than pushing the table into its own horizontal scroller — measured on a
|
|
11
|
+
// real catalogue at 390px, where four columns of full-length titles, tag
|
|
12
|
+
// lists and collection lists took the table to 1,612px and every row
|
|
13
|
+
// scrolled sideways.
|
|
7
14
|
import {
|
|
8
15
|
Table as TableRoot,
|
|
9
16
|
TableBody,
|
|
@@ -31,16 +38,28 @@
|
|
|
31
38
|
const hasBadges = $derived(documents.some((d) => d.badges?.length));
|
|
32
39
|
</script>
|
|
33
40
|
|
|
34
|
-
|
|
41
|
+
<!-- The card surface, not a bare border. This wrapper holds CONTENT, so it has
|
|
42
|
+
to paint one: with `border` alone the table and every ancestor computed
|
|
43
|
+
rgba(0,0,0,0) at 1440x900 in both themes, so the page background showed
|
|
44
|
+
straight through a container sitting beside opaque cards. `bg-card
|
|
45
|
+
border-border ds-edge` is the same trio Panel, StatCard, StatList,
|
|
46
|
+
DetailPanel, EmptyState and DataTableTanstack use — and `border-border`
|
|
47
|
+
matters on its own: a bare `border` resolves to currentColor, which was
|
|
48
|
+
drawing this hairline in full-strength foreground rather than the border
|
|
49
|
+
token. `overflow-hidden` comes with the surface, as it does on every
|
|
50
|
+
sibling: a row's `hover:bg-muted/50` otherwise paints square corners over
|
|
51
|
+
the wrapper's radius now that there is a surface to paint over.
|
|
52
|
+
Gated by src/test/bordered-surface.test.ts. -->
|
|
53
|
+
<div class="bg-card border-border ds-edge overflow-hidden rounded-md border">
|
|
35
54
|
<TableRoot>
|
|
36
55
|
<TableHeader>
|
|
37
56
|
<TableRow>
|
|
38
57
|
<TableHead>Title</TableHead>
|
|
39
58
|
{#if hasTags}
|
|
40
|
-
<TableHead class="w-40">Tags</TableHead>
|
|
59
|
+
<TableHead class="hidden w-40 xl:table-cell">Tags</TableHead>
|
|
41
60
|
{/if}
|
|
42
61
|
{#if hasCollections}
|
|
43
|
-
<TableHead class="w-40">Collections</TableHead>
|
|
62
|
+
<TableHead class="hidden w-40 lg:table-cell">Collections</TableHead>
|
|
44
63
|
{/if}
|
|
45
64
|
{#if hasBadges}
|
|
46
65
|
<TableHead class="w-40 text-center">Status</TableHead>
|
|
@@ -50,11 +69,16 @@
|
|
|
50
69
|
<TableBody>
|
|
51
70
|
{#each documents as doc (doc.id)}
|
|
52
71
|
<TableRow>
|
|
53
|
-
|
|
72
|
+
<!-- max-w-0 + truncate, together: TableCell sets whitespace-nowrap, so
|
|
73
|
+
without a width bound this column stretches the table to its widest
|
|
74
|
+
title, and with a bound but no truncate the text overflows UNDER the
|
|
75
|
+
next cell — which then swallows the link's clicks. -->
|
|
76
|
+
<TableCell class="max-w-0">
|
|
54
77
|
{#if documentHref}
|
|
55
78
|
<a
|
|
56
79
|
href={documentHref(doc)}
|
|
57
|
-
|
|
80
|
+
title={doc.title}
|
|
81
|
+
class="block truncate font-medium hover:underline"
|
|
58
82
|
onclick={() => onOpen?.(doc)}
|
|
59
83
|
>
|
|
60
84
|
{doc.title}
|
|
@@ -62,22 +86,29 @@
|
|
|
62
86
|
{:else if onOpen}
|
|
63
87
|
<button
|
|
64
88
|
type="button"
|
|
65
|
-
|
|
89
|
+
title={doc.title}
|
|
90
|
+
class="block w-full truncate text-left font-medium hover:underline"
|
|
66
91
|
onclick={() => onOpen(doc)}
|
|
67
92
|
>
|
|
68
93
|
{doc.title}
|
|
69
94
|
</button>
|
|
70
95
|
{:else}
|
|
71
|
-
<span class="font-medium">{doc.title}</span>
|
|
96
|
+
<span class="block truncate font-medium" title={doc.title}>{doc.title}</span>
|
|
72
97
|
{/if}
|
|
73
98
|
</TableCell>
|
|
74
99
|
{#if hasTags}
|
|
75
|
-
<TableCell
|
|
100
|
+
<TableCell
|
|
101
|
+
class="text-muted-foreground hidden max-w-40 truncate text-xs xl:table-cell"
|
|
102
|
+
title={doc.tags?.join(', ')}
|
|
103
|
+
>
|
|
76
104
|
{doc.tags?.join(', ') || '—'}
|
|
77
105
|
</TableCell>
|
|
78
106
|
{/if}
|
|
79
107
|
{#if hasCollections}
|
|
80
|
-
<TableCell
|
|
108
|
+
<TableCell
|
|
109
|
+
class="text-muted-foreground hidden max-w-40 truncate text-xs lg:table-cell"
|
|
110
|
+
title={doc.collections?.join(', ')}
|
|
111
|
+
>
|
|
81
112
|
{doc.collections?.join(', ') || '—'}
|
|
82
113
|
</TableCell>
|
|
83
114
|
{/if}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { cn, type WithElementRef } from '../../../utils.js';
|
|
16
16
|
import Search from '@lucide/svelte/icons/search';
|
|
17
17
|
import LibraryIcon from '@lucide/svelte/icons/library';
|
|
18
|
+
import SlidersHorizontal from '@lucide/svelte/icons/sliders-horizontal';
|
|
18
19
|
import X from '@lucide/svelte/icons/x';
|
|
19
20
|
import { Input } from '../input/index.js';
|
|
20
21
|
import Button from '../button/button.svelte';
|
|
@@ -110,6 +111,15 @@
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Whether the facet rail is revealed below `md`. The only state this
|
|
116
|
+
* component owns, and deliberately so: it is a disclosure, not a filter.
|
|
117
|
+
* The rail used to be `hidden md:block` with nothing to reveal it, so on a
|
|
118
|
+
* phone the whole of filtering was simply absent — no drawer, no
|
|
119
|
+
* disclosure, no way to reach it at all.
|
|
120
|
+
*/
|
|
121
|
+
let facetsOpen = $state(false);
|
|
122
|
+
|
|
113
123
|
const shownTotal = $derived(total ?? documents.length);
|
|
114
124
|
// Floored at 1 so a consumer's limit: 0 cannot put Infinity in the pager.
|
|
115
125
|
const pageSize = $derived(Math.max(1, limit));
|
|
@@ -161,11 +171,24 @@
|
|
|
161
171
|
</div>
|
|
162
172
|
{/if}
|
|
163
173
|
|
|
174
|
+
{#if facets.length > 0}
|
|
175
|
+
<Button
|
|
176
|
+
variant="outline"
|
|
177
|
+
size="sm"
|
|
178
|
+
class="w-fit md:hidden"
|
|
179
|
+
aria-expanded={facetsOpen}
|
|
180
|
+
onclick={() => (facetsOpen = !facetsOpen)}
|
|
181
|
+
>
|
|
182
|
+
<SlidersHorizontal class="size-4" />
|
|
183
|
+
{facetsOpen ? 'Hide filters' : 'Filters'}
|
|
184
|
+
</Button>
|
|
185
|
+
{/if}
|
|
186
|
+
|
|
164
187
|
<div
|
|
165
188
|
class={facets.length > 0 ? 'grid gap-6 md:grid-cols-[220px_minmax(0,1fr)]' : 'grid gap-6'}
|
|
166
189
|
>
|
|
167
190
|
{#if facets.length > 0}
|
|
168
|
-
<aside class=
|
|
191
|
+
<aside class={facetsOpen ? 'block' : 'hidden md:block'} aria-label="Filters">
|
|
169
192
|
<FacetRail {facets} onChange={onFacetChange} />
|
|
170
193
|
</aside>
|
|
171
194
|
{/if}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@poodle64/ui",
|
|
3
|
-
"version": "2026.8.
|
|
3
|
+
"version": "2026.8.15",
|
|
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,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.
|
|
4
|
+
Generated from `@poodle64/ui@2026.8.15`. 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
|
|