@poodle64/ui 2026.9.21 → 2026.9.23

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
@@ -1411,6 +1411,6 @@ non-zero and naming the field on any difference.
1411
1411
  1. Change a component; bump `version` in `package.json` (CalVer).
1412
1412
  2. `pnpm build`, which runs `svelte-package` then `publint` (package.json/exports sanity).
1413
1413
  3. Commit, tag `ui-v<version>`, push the tag.
1414
- 4. `.github/workflows/publish.yaml` runs on that push and publishes via npm
1415
- OIDC trusted publishing: `gh run list --workflow=publish.yaml`.
1414
+ 4. `.github/workflows/publish-kit-typescript.yaml` (repo root) runs on that push and publishes via npm
1415
+ OIDC trusted publishing: `gh run list -R radar-hooves/app-factory --workflow=publish-kit-typescript.yaml`.
1416
1416
  Confirm with `npm view @poodle64/ui version`.
@@ -24,6 +24,8 @@
24
24
  // components/ui — it is app machinery the layout mounts once (alongside
25
25
  // initBrowserTelemetry), not page chrome a route composes from, so it does
26
26
  // not sit in the situation→component registry either.
27
+ // A page control that shares its corner sets `--ds-report-clearance` on the
28
+ // document to lift the trigger clear of it (the librarian composer does).
27
29
 
28
30
  let {
29
31
  /** Where the report POSTs. The app's own route, defaulting to the one every household app serves. */
@@ -105,7 +107,7 @@
105
107
  <button
106
108
  type="button"
107
109
  onclick={() => (open = true)}
108
- class="border-border bg-card text-muted-foreground hover:text-foreground ds-edge fixed right-4 bottom-[calc(1rem+env(safe-area-inset-bottom))] z-50 grid size-11 flex-none place-items-center rounded-full border transition-colors"
110
+ class="border-border bg-card text-muted-foreground hover:text-foreground ds-edge fixed right-4 bottom-[calc(1rem+var(--ds-report-clearance,0px)+env(safe-area-inset-bottom))] z-50 grid size-11 flex-none place-items-center rounded-full border transition-colors"
109
111
  aria-label="Report a problem"
110
112
  >
111
113
  <MessageSquarePlus class="size-4.5" />
@@ -150,98 +150,115 @@
150
150
  class="flex-1 rounded-none border-0"
151
151
  />
152
152
  {:else}
153
- <div class="min-h-0 flex-1 overflow-auto">
154
- <TableRoot class="w-full">
155
- <TableHeader class="bg-card sticky top-0 z-10">
156
- {#each headerGroups as headerGroup (headerGroup.id)}
157
- <TableRow class="border-0 hover:bg-transparent">
158
- {#if selectable}
159
- <TableHead class="border-border w-10 border-b px-3 py-1.5">
160
- <Checkbox
161
- checked={allSelected}
162
- indeterminate={someSelected}
163
- onCheckedChange={toggleAll}
164
- aria-label="Select all rows"
153
+ <!-- containerClass targets table.svelte's own scroll container (`data-slot=
154
+ table-container`), which otherwise carries only `overflow-x-auto` — and
155
+ `overflow-x` other than visible forces `overflow-y` to compute `auto`
156
+ too, making that div a scroll container in its own right even though it
157
+ never actually scrolls (it is always sized to its own content). A
158
+ `position: sticky` header's containing block is its NEAREST ancestor
159
+ scroll container, so with no override here the header (radar-hooves/
160
+ godswood, 30/09/2026) sticks to that inert box and never visibly pins,
161
+ in every engine, not only WebKit — confirmed with this repo's own
162
+ Playwright (chromium and webkit) driving the unmodified component.
163
+ Folding this component's own intended scroll region into that SAME box
164
+ (rather than wrapping a second one around it) makes it the one thing
165
+ that actually scrolls, so sticky has a real ancestor to pin against. -->
166
+ <TableRoot class="w-full" containerClass="min-h-0 flex-1 overflow-y-auto">
167
+ <!-- Sticky lives on each `<th>`, not on `<thead>`: WebKit has a long
168
+ history of not reliably keeping `position: sticky` on a table-section
169
+ box (`<thead>`) pinned, while sticky on the `<th>` cells — which
170
+ participate in table layout as ordinary boxes — sticks correctly. -->
171
+ <TableHeader>
172
+ {#each headerGroups as headerGroup (headerGroup.id)}
173
+ <TableRow class="border-0 hover:bg-transparent">
174
+ {#if selectable}
175
+ <TableHead
176
+ class="border-border bg-card sticky top-0 z-10 w-10 border-b px-3 py-1.5"
177
+ >
178
+ <Checkbox
179
+ checked={allSelected}
180
+ indeterminate={someSelected}
181
+ onCheckedChange={toggleAll}
182
+ aria-label="Select all rows"
183
+ />
184
+ </TableHead>
185
+ {/if}
186
+ {#each headerGroup.headers as header (header.id)}
187
+ {@const meta = header.column.columnDef.meta}
188
+ <TableHead
189
+ class={cn(
190
+ 'text-muted-foreground border-border bg-card sticky top-0 z-10 text-2xs tracking-eyebrow border-b px-3 py-1.5 text-left font-medium whitespace-nowrap uppercase',
191
+ meta?.class,
192
+ meta?.headClass
193
+ )}
194
+ >
195
+ {#if !header.isPlaceholder}
196
+ <FlexRender
197
+ content={header.column.columnDef.header}
198
+ context={header.getContext()}
165
199
  />
166
- </TableHead>
167
- {/if}
168
- {#each headerGroup.headers as header (header.id)}
169
- {@const meta = header.column.columnDef.meta}
170
- <TableHead
171
- class={cn(
172
- 'text-muted-foreground border-border text-2xs tracking-eyebrow border-b px-3 py-1.5 text-left font-medium whitespace-nowrap uppercase',
173
- meta?.class,
174
- meta?.headClass
175
- )}
176
- >
177
- {#if !header.isPlaceholder}
178
- <FlexRender
179
- content={header.column.columnDef.header}
180
- context={header.getContext()}
181
- />
182
- {/if}
183
- </TableHead>
184
- {/each}
185
- </TableRow>
186
- {/each}
187
- </TableHeader>
200
+ {/if}
201
+ </TableHead>
202
+ {/each}
203
+ </TableRow>
204
+ {/each}
205
+ </TableHeader>
188
206
 
189
- <TableBody>
190
- {#each rows as row (row.id)}
191
- {@const originalRow = row.original}
192
- {@const rowId = getRowId(originalRow)}
193
- {@const isSelected = rowId === selectedId}
194
- {@const isChecked = selectedSet.has(rowId)}
195
- {@const extraStyle = getRowStyle ? getRowStyle(originalRow) : ''}
196
- <TableRow
197
- data-state={isSelected ? 'selected' : undefined}
198
- class="border-border/60 border-b last:border-0 {onSelect
199
- ? 'cursor-pointer hover:bg-[color-mix(in_oklch,var(--foreground)_4%,transparent)]'
200
- : ''} {isSelected ? 'bg-[color-mix(in_oklch,var(--primary)_10%,transparent)]' : ''}"
201
- style="{extraStyle}{isSelected ? ' ' + selectedShadow : ''}"
202
- onclick={onSelect ? () => onSelect(rowId) : undefined}
203
- onkeydown={onSelect
204
- ? (e) => {
205
- if (e.key === 'Enter' || e.key === ' ') {
206
- e.preventDefault();
207
- onSelect(rowId);
208
- }
207
+ <TableBody>
208
+ {#each rows as row (row.id)}
209
+ {@const originalRow = row.original}
210
+ {@const rowId = getRowId(originalRow)}
211
+ {@const isSelected = rowId === selectedId}
212
+ {@const isChecked = selectedSet.has(rowId)}
213
+ {@const extraStyle = getRowStyle ? getRowStyle(originalRow) : ''}
214
+ <TableRow
215
+ data-state={isSelected ? 'selected' : undefined}
216
+ class="border-border/60 border-b last:border-0 {onSelect
217
+ ? 'cursor-pointer hover:bg-[color-mix(in_oklch,var(--foreground)_4%,transparent)]'
218
+ : ''} {isSelected ? 'bg-[color-mix(in_oklch,var(--primary)_10%,transparent)]' : ''}"
219
+ style="{extraStyle}{isSelected ? ' ' + selectedShadow : ''}"
220
+ onclick={onSelect ? () => onSelect(rowId) : undefined}
221
+ onkeydown={onSelect
222
+ ? (e) => {
223
+ if (e.key === 'Enter' || e.key === ' ') {
224
+ e.preventDefault();
225
+ onSelect(rowId);
209
226
  }
210
- : undefined}
211
- tabindex={onSelect ? 0 : undefined}
212
- >
213
- {#if selectable}
214
- <TableCell class="w-10 px-3 py-1.5">
215
- <!-- The wrapper ONLY stops the event reaching the row, so ticking a
216
- checkbox never also opens the master-detail. The checkbox's own
217
- onCheckedChange is the single source of the selection change —
218
- handling the click here as well would toggle twice per click and
219
- net out to no selection at all. -->
220
- <div
221
- role="presentation"
222
- onclick={(e) => e.stopPropagation()}
223
- onkeydown={(e) => e.stopPropagation()}
224
- >
225
- <Checkbox
226
- checked={isChecked}
227
- onCheckedChange={(v) => setRowSelected(rowId, v)}
228
- aria-label="Select row"
229
- />
230
- </div>
231
- </TableCell>
232
- {/if}
233
- {#each row.getVisibleCells() as cell (cell.id)}
234
- {@const meta = cell.column.columnDef.meta}
235
- <TableCell
236
- class={cn('px-3 py-1.5 text-sm whitespace-nowrap', meta?.class, meta?.cellClass)}
227
+ }
228
+ : undefined}
229
+ tabindex={onSelect ? 0 : undefined}
230
+ >
231
+ {#if selectable}
232
+ <TableCell class="w-10 px-3 py-1.5">
233
+ <!-- The wrapper ONLY stops the event reaching the row, so ticking a
234
+ checkbox never also opens the master-detail. The checkbox's own
235
+ onCheckedChange is the single source of the selection change —
236
+ handling the click here as well would toggle twice per click and
237
+ net out to no selection at all. -->
238
+ <div
239
+ role="presentation"
240
+ onclick={(e) => e.stopPropagation()}
241
+ onkeydown={(e) => e.stopPropagation()}
237
242
  >
238
- <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
239
- </TableCell>
240
- {/each}
241
- </TableRow>
242
- {/each}
243
- </TableBody>
244
- </TableRoot>
245
- </div>
243
+ <Checkbox
244
+ checked={isChecked}
245
+ onCheckedChange={(v) => setRowSelected(rowId, v)}
246
+ aria-label="Select row"
247
+ />
248
+ </div>
249
+ </TableCell>
250
+ {/if}
251
+ {#each row.getVisibleCells() as cell (cell.id)}
252
+ {@const meta = cell.column.columnDef.meta}
253
+ <TableCell
254
+ class={cn('px-3 py-1.5 text-sm whitespace-nowrap', meta?.class, meta?.cellClass)}
255
+ >
256
+ <FlexRender content={cell.column.columnDef.cell} context={cell.getContext()} />
257
+ </TableCell>
258
+ {/each}
259
+ </TableRow>
260
+ {/each}
261
+ </TableBody>
262
+ </TableRoot>
246
263
  {/if}
247
264
  </div>
package/package.json CHANGED
@@ -1,17 +1,18 @@
1
1
  {
2
2
  "name": "@poodle64/ui",
3
- "version": "2026.9.21",
3
+ "version": "2026.9.23",
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",
7
7
  "repository": {
8
8
  "type": "git",
9
- "url": "git+https://github.com/radar-hooves/design-system.git"
9
+ "url": "git+https://github.com/radar-hooves/app-factory.git",
10
+ "directory": "kits/typescript/packages/ui"
10
11
  },
11
12
  "publishConfig": {
12
13
  "registry": "https://registry.npmjs.org",
13
14
  "access": "public",
14
- "provenance": false
15
+ "provenance": true
15
16
  },
16
17
  "files": [
17
18
  "bin",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "meta": {
3
3
  "package": "@poodle64/ui",
4
- "version": "2026.9.21",
4
+ "version": "2026.9.23",
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": 61,
@@ -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.9.21`. 61 components, 17 situations.
4
+ Generated from `@poodle64/ui@2026.9.23`. 61 components, 17 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