@poodle64/ui 2026.8.13 → 2026.8.14

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.
@@ -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
- <div class="grid gap-4 sm:grid-cols-2">
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 class={field.mono ? 'truncate font-mono text-xs' : 'text-right'}>
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">{location.path}</span>
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,
@@ -37,10 +44,10 @@
37
44
  <TableRow>
38
45
  <TableHead>Title</TableHead>
39
46
  {#if hasTags}
40
- <TableHead class="w-40">Tags</TableHead>
47
+ <TableHead class="hidden w-40 xl:table-cell">Tags</TableHead>
41
48
  {/if}
42
49
  {#if hasCollections}
43
- <TableHead class="w-40">Collections</TableHead>
50
+ <TableHead class="hidden w-40 lg:table-cell">Collections</TableHead>
44
51
  {/if}
45
52
  {#if hasBadges}
46
53
  <TableHead class="w-40 text-center">Status</TableHead>
@@ -50,11 +57,16 @@
50
57
  <TableBody>
51
58
  {#each documents as doc (doc.id)}
52
59
  <TableRow>
53
- <TableCell>
60
+ <!-- max-w-0 + truncate, together: TableCell sets whitespace-nowrap, so
61
+ without a width bound this column stretches the table to its widest
62
+ title, and with a bound but no truncate the text overflows UNDER the
63
+ next cell — which then swallows the link's clicks. -->
64
+ <TableCell class="max-w-0">
54
65
  {#if documentHref}
55
66
  <a
56
67
  href={documentHref(doc)}
57
- class="font-medium hover:underline"
68
+ title={doc.title}
69
+ class="block truncate font-medium hover:underline"
58
70
  onclick={() => onOpen?.(doc)}
59
71
  >
60
72
  {doc.title}
@@ -62,22 +74,29 @@
62
74
  {:else if onOpen}
63
75
  <button
64
76
  type="button"
65
- class="text-left font-medium hover:underline"
77
+ title={doc.title}
78
+ class="block w-full truncate text-left font-medium hover:underline"
66
79
  onclick={() => onOpen(doc)}
67
80
  >
68
81
  {doc.title}
69
82
  </button>
70
83
  {:else}
71
- <span class="font-medium">{doc.title}</span>
84
+ <span class="block truncate font-medium" title={doc.title}>{doc.title}</span>
72
85
  {/if}
73
86
  </TableCell>
74
87
  {#if hasTags}
75
- <TableCell class="text-muted-foreground text-xs">
88
+ <TableCell
89
+ class="text-muted-foreground hidden max-w-40 truncate text-xs xl:table-cell"
90
+ title={doc.tags?.join(', ')}
91
+ >
76
92
  {doc.tags?.join(', ') || '—'}
77
93
  </TableCell>
78
94
  {/if}
79
95
  {#if hasCollections}
80
- <TableCell class="text-muted-foreground text-xs">
96
+ <TableCell
97
+ class="text-muted-foreground hidden max-w-40 truncate text-xs lg:table-cell"
98
+ title={doc.collections?.join(', ')}
99
+ >
81
100
  {doc.collections?.join(', ') || '—'}
82
101
  </TableCell>
83
102
  {/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="hidden md:block" aria-label="Filters">
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.13",
3
+ "version": "2026.8.14",
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
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.8.13",
4
+ "version": "2026.8.14",
5
5
  "generatedBy": "scripts/generate-registry.mjs",
6
6
  "source": "scripts/situations.json + package source (DO NOT EDIT the outputs by hand)",
7
7
  "componentCount": 54,
@@ -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.13`. 54 components, 14 situations.
4
+ Generated from `@poodle64/ui@2026.8.14`. 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