Step 12e: shadcn-svelte as the app's component base #25
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Owner decision (2026-09-24): "I don't wanna vibecode the basics." shadcn-svelte (Svelte 5, bits-ui, Tailwind v4) becomes the component base for the whole app, starting with its Sidebar. Drag-and-drop reordering in the sidebar is essential and must be built on top of it. Reuse sveltebits.xyz components wherever they fit, instead of writing them from scratch. The goal is to spend effort on the backend, not on UI basics.
At the time of the decision, Tailwind 4.3.3 is the latest release on npm and shadcn-svelte is 1.7.0.
Starting step 12e on branch
shadcn, based ondevat7bda001e5bd8ff42c3768fb1e6cbba317d22789e. I’ll migrate the owned UI surfaces to shadcn-svelte, preserve the docs/UI.md behavior and tokens, and keep clear of the container detail, Terminals, and stream client files owned bylive-web.Svelte Bits license finding: upstream
DavidHDev/svelte-bitsdeclares MIT plus Commons Clause (https://github.com/DavidHDev/svelte-bits/blob/main/LICENSE.md); the Commons Clause FAQ says the additional restriction is on selling the software (https://commonsclause.com/). DECISIONS.md §3 requires code in this repo to be AGPL. I will not copy/import Svelte Bits components into this app under the current settled licensing decision; this avoids adding source with incompatible terms. The shadcn-svelte primitives and our own small compositions remain in scope.The shadcn-svelte 1.7.0 initializer refused the first run because Tailwind CSS v4 was not installed, then completed once Tailwind CSS and
@tailwindcss/vite4.3.3 were added. I selected the Vega preset (Inter/Lucide) with neutral base colors to fit the existing Inter UI font and neutral surface tokens.packages/web/components.jsonpoints component sources and utility aliases intopackages/ui; this keeps the shared package as the single component source.Foundation verification:
scripts/verify fastpassed all configured checks. The shared stylesheet atpackages/ui/src/tokens.cssis compiled by the v4.3.3 Vite plugin; the compiler is also a UI-package dev dependency because the stylesheet is resolved from that workspace.bun run --cwd=packages/ui checkreports zero errors/warnings andbun run --cwd=packages/web buildpasses. I removed generated broad transition classes and pulse effects to keep UI motion within the transform/opacity contract.Added the generated shadcn Alert primitive and moved the shared ErrorState composition to Alert semantics. VirtualTable now uses the generated shadcn Table structure while retaining TanStack virtualization and bounded rows. The UI fixture targets the labeled table body and Bits UI Select's actual button role;
bun run --cwd=packages/ui testpasses all 5 tests, and the UI Svelte check reports 0 errors and 0 warnings.The first browser runs surfaced two integration issues with concrete fixes:
packages/terminalimports the sharedpackages/ui/src/tokens.css; that stylesheet now importsshadcn-svelte/tailwind.css, soshadcn-sveltemust be a development dependency ofpackages/ui, where the CSS import resolves. After moving the dependency,PASS browser: terminal fixture Playwright.Card.Titlerenders adiv, so it did not provide the auth screen's required level-one heading.AuthScreen.sveltenow puts a semantic<h1>inside the shadcn title container;svelte-checkreports 0 errors and 0 warnings.The full release browser gate is being rerun. I paused my second release compile after a separate integration job started its own build, following the shared-VM single-build rule; I will resume when that build exits.
The release browser run now passes the setup and sign-in screens after the auth selectors were scoped to
#setup-usernameand#signin-username. It then exposed a brittle container fixture lookup: the UI table is virtualized and this VM already has many benchmark containers, so the new browser fixture was not among the mounted rows.scripts/browser-check.mjsnow searches for that fixture by name through the Containers page's Search field before exercising stop/start. Re-running the production browser gate after the parallel release build exits.Cold dashboard bundle finding from the production browser check: initial route JavaScript measured 192,615 bytes gzip against the 120 KiB (122,880 byte) budget. The pre-migration measurement was 76,757 bytes. The route graph includes shared shadcn Button/Card code plus the shell's sidebar interaction code. I have split the sortable/group-management sidebar into an on-intent module while leaving its normal navigation and account footer available, and moved auth/inbox surfaces behind their relevant states. The next browser run will provide the after measurement; no completion claim yet.
Checkpoint
e7afb1e: after deferring the collapsed-sidebar Tooltip/Popper module, the production static dashboard import graph measures 115,337 gzip bytes against the 122,880-byte limit. This is a manifest graph estimate; the browser gate's actual response metric is still pending.bun --cwd=packages/ui run checkandbun --cwd=packages/web run checkboth report 0 errors and 0 warnings.The first browser-gate run stopped in the separate terminal fixture before the web build/app checks: xterm's Chromium IME composition test expected
日本but timed out with only carriage-return data. I reran that single Playwright case (--grep 'xterm: receives a Chromium IME composition' --workers=1); it passed in 4.3 seconds. The failure did not reproduce in isolation, and no terminal package files were changed. I am rerunningscripts/verify browserto check the full fixture and app suite.The second
scripts/verify browserrun passed its terminal fixture, production web build and release binary build. The real browser measurement was 116,682 bytes gzip for the initial route (budget 122,880). The app check then failed on the collapsed Hosts tooltip. I added adata-tooltip-readystate and changed the check to wait for the optional Tooltip chunk and hover the actual Hosts link; this will separate lazy-load timing from a target-overlap issue on the next run. UI and web Svelte checks both passed with 0 errors and 0 warnings after the change.The third
scripts/verify browserrun measured 116,697 bytes gzip (budget 122,880) and isolated the collapsed Hosts tooltip failure: Playwright reported that the reorder handle intercepted pointer events over the nav link. I widened the icon rail to 72px so the 32px nav link and 24px reorder handle have separate hit targets, and adjusted the collapsed footer controls to remain on one row. I’m running the focused Svelte checks next; the browser gate will be rerun after the current integration release compile exits.The fourth
scripts/verify browserrun passed the updated link/reorder geometry and measured 116,698 bytes gzip, then failed only because the browser assertion searched forrole="tooltip". I exercised the current production binary directly: hovering the collapsed Hosts link sets its Bits UIdata-statetoinstant-open, renders visible tooltip content, and connects it witharia-describedby. Bits UI documentsdata-tooltip-contentas its content marker, so I changed the gate to assert the trigger's open state and visible content marker instead of a role this component does not emit.The browser gate now passes the collapsed link/handle layout and Tooltip checks, and the actual initial-route measure remains 116,698 bytes gzip. It advanced into keyboard sorting, where the
Hostshandle locator matched two rendered buttons and caused a Playwright strict-mode failure. I constrained the keyboard drag checks to the first matching item/group handle; the next browser run will verify the drag assertions and finish the requested screenshot matrix.The last browser run measured 116,698 bytes gzip and passed the tooltip and keyboard handle selectors. Keyboard pickup then exposed that the live region was still using dnd-kit's default wording rather than the configured navigation label. I changed the provider to extend its default plugins with
Accessibility.configure(...)per the official Svelte plugin API, andbun --cwd=packages/web run checkpassed with 0 errors and 0 warnings. I’ll verify the label announcements and screenshot matrix in the next browser run.The seventh browser run confirmed the Svelte provider initially constructs its default manager before applying the reactive
pluginsprop, so Accessibility's constructor-captured announcements remained default. I now create theDragDropManagerwith the configured default plugin set and pass it into the provider, then destroy it from the Svelte component's teardown.bun --cwd=packages/web run checkreports 0 errors and 0 warnings. I’ll verify that the live region now announces “Hosts picked up.” on the next browser run.The final browser gate is waiting on shared-VM capacity. An eight-worker
stress-ngrun and an external release compile have kept load averages above 10, so I’m not starting another release build or browser suite under contention. Current branch checkpoint:f20fcc5; websvelte-checkis clean. I’ll run the final gates and capture/review screenshots once the VM is available.The eighth browser run measured 116,697 bytes gzip and confirmed the custom Accessibility plugin is now active: the live region used the navigation label “Hosts”. It failed because the first keyboard move onto the source item itself announced “Hosts moved over Hosts.” I’m skipping announcements when source and target IDs match, preserving the “Hosts picked up.” message until the item moves to another target. The final browser run still needs to verify this and capture the requested screenshot matrix.
The release browser run measured 116,697 bytes gzip and passed the terminal fixture, app build, release build, and inbox geometry checks up to one incorrect mask assertion. The base
.inbox-overlayscrim correctly hasmask-image: none; the::beforeand::afterblur layers each have their own gradient mask and bounded geometry. I removed the root-mask check and kept the existing per-layer mask checks.The next production browser run confirmed the inbox Dialog opens and its blur checks pass, but Escape did not return focus to the sidebar trigger. The surface is a controlled Dialog mounted separately from the trigger, so it has no Dialog.Trigger relationship for automatic return focus. I’m passing the invoking button from both sidebar variants into AppShell and restoring focus after close; web svelte-check reports 0 errors and 0 warnings.
The production run now passes inbox Escape focus restoration, then found the page header's 12px pseudo-element blur still active under the inbox scrim.
docs/UI.mdspecifies that opening the inbox suspends page chrome blur while retaining a static tint. I bound the header toinboxOpenand disable the base, 6px and 12px page-header filters while open; websvelte-checkpasses with 0 errors and 0 warnings.The production browser gate now passes inbox focus restoration and page-chrome blur suspension. Drag capture then found two
.nav-group[data-drop-group="access"]nodes during a drag: the visible source and an inert[data-dnd-placeholder="hidden"]. Updated the screenshot assertion to exclude the placeholder; commit2875bdb.Browser evidence: the pointer drag reorders Access before Infrastructure while the group is still active. The old browser assertion kept targeting Infrastructure at its pre-drag screen position; that point now contains the dragged Access group. The check now asserts the live order and captures before release. The primitive focus fixture also uses a shadcn/Bits UI Popover, which is non-modal and has no dialog role; the browser check now follows its rendered “Open details” trigger and generated content slot while preserving entry, Escape-close and focus-return checks. Committed as
da41713.Repeated Chromium scroll-smoke measurements of the inbox overlay were 87–116 ms p95 with the 2/6/12 px backdrop filters, versus about 17 ms with filters disabled. Since the requested progressive blur was conditional on fitting the frame-time budget, I removed filtering from the full-page inbox scrim and kept its masked static tint and card shadow.
docs/UI.mdrecords the evidence. The browser gate now asserts the inbox has no backdrop filters and measures the smaller top-bar treatment separately before deciding whether it stays.The production browser run reached the container table and measured its 32 px sortable header buttons extending 3 px below their 40 px header cells (1440×900). Reduced only the header's vertical padding from 12 px to 4 px so the controls fit. The table fit check now distinguishes a normal partially visible row at its internal scroll boundary from controls that exceed their own row. UI and web Svelte checks both passed; final fast/browser gates remain pending.
The browser gate exposed a fixture/test mismatch in the virtualized Containers list: the temporary browser container was present in the inventory but outside the rows currently mounted in the DOM, so waiting on it without Search could never pass. Commit
1c0beb6updates the checks to use a visible row for host/engine filter assertions, verify the filtered row addresses, and apply Search only for fixture actions. Responsive checks clear Search and wait for the full inventory response before capturing the page. Post-commitscripts/verify fastpassed all checks; the final browser gate is running next.The browser gate reached medium-width layout after passing the bundle, font and scroll-smoke checks. Its sidebar clipping assertion treated shadcn's
Sidebar.Raillike an in-sidebar control, but the generated 16 px rail is deliberately centered on the sidebar edge (8 px on either side). Commit14d8517excludes that rail from the inside-bounds check and explicitly verifies that it stays visible, centered on the sidebar boundary and within the viewport. The browser gate is being rerun.The browser gate passed the explicit medium-width rail geometry check. It then reached mobile and exposed another stale selector in the screenshot/focus helper: shadcn's mobile Sidebar renders its Sheet
Contentas the.app-sidebarroot, but the check searched for a nested.app-sidebarinside theSidebardialog. Commit423790bscopes the 390px and 320px account-row and drag checks to the dialog root and retains response URL/status details if the responsive Search reset fails later. The browser gate is rerunning.The mobile sidebar selector change passed through to the responsive container checks. Captured responses showed the Search reset only receiving periodic
304responses because the test kept the input focused; this field reloads on nativechange, which commits on blur. Commit14b7bdaintroduces a shared browser helper that Tabs out after each Search set/clear and waits for the matching200response before inspecting or capturing the inventory.node --checkandgit diff --checkpass; full fast and browser gates remain.The latest browser run passed the medium-width rail check, mobile drawer/account-row checks and Search response checks, then reached touch sorting. The test saw
pointerType=touchbut moved immediately aftertouchStart; the installed@dnd-kit/dom0.5.0 PointerSensor requires a 250 ms touch activation delay within a 5 px tolerance, so the test canceled activation before the drop target could resolve. Commit1cf2fbfwaits for the drag handle'saria-pressed=truestate before dispatching movement. The next browser run will verify the touch reorder and finish the screenshot matrix.The next browser run activated the dnd-kit touch drag, then showed the
Hostshandle locator matched two nodes: the first wasaria-pressed=trueat the dispatched touch start, while the second remained false. Commit58ec84epins touch geometry and activation assertions to the first handle/drop row, matching the coordinates used to start the gesture.node --checkandgit diff --checkpass; the browser gate must verify the drop target and reorder next.The latest touch run activates and sees the
Containersrow as its drop target, butHostsremains in the original position after release. Commitb825b47adds a check for the expected order while the drag is still active. This will distinguish whether liveonDragOvermutation failed or the order was rolled back attouchEnd; the next browser gate will provide that evidence.The latest diagnostic read
hosts, hosts, containerswhile dragging. Inspection of the installed dnd-kit Feedback plugin showed that it inserts anaria-hidden[data-dnd-placeholder]clone of the source at its original slot. The order reader counted that clone, so the in-drag assertion did not establish whether the application order changed. Commit16667eefilters placeholder clones from item and group order checks and selects real targets; the next browser run will verify the actual order before and after touch release.Browser tracing confirmed that mobile touch sorting oscillates on a same-group target. For a drag of Hosts below Containers, dnd-kit first produces
[Containers, Hosts, ...]; a repeated hover event over Containers then restores[Hosts, Containers, ...]. The root cause is@dnd-kit/helpersmove()usingarrayMove(sourceIndex, targetIndex)for same-parent targets without accounting for which half of the target the pointer occupies. Cross-group handling already accounts for pointer position. I’m adding stable pointer-half placement for same-group item and group sorting, retainingmove()for cross-group moves, with unit coverage and a browser rerun.The rerun of
scripts/verify browserbuilt the production binary and measured 116,910 bytes gzip, then failed the keyboard sort assertion: after Space + ArrowDown, the sidebar order stayed unchanged. The new same-group helper sees dnd-kit's keyboard drop target as the source itself, so it no-ops instead of committing the sortable's projected index. I verified the installed Svelte adapter removesOptimisticSortingPluginfrom each sortable's default plugins, so the review's optimistic-DOM conflict does not apply to this adapter; I’m keepingmove(items, event)for keyboard drags and reserving pointer-half placement for pointer/touch drags. Official provider docs describe theactivatorEventandpreventDefaultcontracts: https://dndkit.com/svelte/components/drag-drop-provider/ ; sortable docs explain keyboard sorting, projected indices, and optimistic plugin behavior: https://dndkit.com/concepts/sortable/ .The keyboard regression is fixed at the source:
onDragOvernow keeps the sortable-awaremove()path for keyboard activations, including self-target events where dnd-kit updates the projected source index. Pointer/touch same-group events use the stable target-half helper, and cross-group moves still use groupedmove(). The installed SveltecreateSortableexplicitly filtersOptimisticSortingPluginfrom defaults, so I did not add event cancellation that could interfere with other drag listeners. Docs are updated; fast and browser gates remain pending on this follow-up.The browser rerun now passes keyboard sorting but fails desktop pointer group reorder: Access stays after Infrastructure when dragged to the header's upper quarter. The dragged group is much taller than its header; comparing the whole group's center to the header center can classify an upper-quarter pointer drop as below. This run measured the initial route at 116,909 gzip bytes; top-bar scroll p95 was 25.5 ms with blur vs 16.8 ms disabled, while inbox was 17.0 ms vs 23.3 ms disabled. I’m checking dnd-kit's sensor position semantics and will use the actual pointer position for group targets if the installed API confirms that is the correct coordinate.
The installed
PointerSensorupdatesdragOperation.positionfrom the current pointer coordinates, whiledragOperation.shaperepresents the dragged element's geometry. That distinction caused the group issue: the dragged group spans its header and every child item, so its center can be below the target header even while the pointer is above it. Same-list pointer/touch placement now comparesposition.current.ywith the target center; keyboard continues using sortable-awaremove().docs/UI.mdlinks the official drag-coordinate API. Load was 11.79 when this fix landed, so the next gate waits for load below 10.Screenshot review: the two static masked pseudo layers left visible rectangular tint bands behind the inbox. Replaced them with one uniform translucent scrim while keeping backdrop filters disabled; the card shadow retains the depth cue. Updated the browser assertion to check visible scrim alpha, card shadow, and no backdrop filter, and updated docs/UI.md.
scripts/verify fastpassed after the change. Commit:28d868b272.The production browser gate still fails the group pointer case. It logs Access at source index 1 over Infrastructure, with pointer y=316.8 and target-shape center y=236; the current helper treats this as below and makes no order change. I added a temporary rectangle trace to confirm the header/group geometry before changing DnD placement.
The independent review of
28d868bfound no CSS behavior defect but identified an incomplete assertion (opaque scrims passed) plus stale masked-tint documentation and dead mobile pseudo-element rules. The assertion now requires a parsed alpha strictly between 0 and 1, no rendered tint pseudo-layers, no backdrop filter and a card shadow; the docs/CSS references are cleaned up. A fresh browser run is pending.The diagnostic browser run confirmed the cause. Infrastructure header bounds are y=136..168, its sortable group bounds are y=136..336 (center y=236), and Access begins at y=346. Playwright moves toward the header in five steps; the first step enters the Infrastructure target at y=316.8, below the group center, so the existing handler chooses the unchanged below-target order. dnd-kit dispatches
dragoveronly when the target ID changes, so it never recomputes while later pointer steps move through the same group to the header's upper quarter. The official Svelte provider exposesonDragMovefor each move (https://dndkit.com/svelte/components/drag-drop-provider/). I will update pointer/touch same-group placement on drag-move using that event'stocoordinates; keyboard and cross-group routing remain on their current paths.Implemented the confirmed drag-over timing fix. Pointer/touch same-group placement now re-evaluates on
onDragMoveusing its incomingtocoordinate; same-section updates copy only the changed list, with no rebuilding of all item buckets per pointer event. Keyboard and cross-group moves remain on the sortable-awaremove()path. Added focused coordinate and section-state tests (the new tests were observed failing before their helpers/store method existed, then passed); targeted DnD tests pass 5/5 and navigation tests pass 3/3.bun --cwd=packages/web run checkpasses with 0 errors and 0 warnings.Fresh
scripts/verify fastoutput:PASS fast: migrations are append-only
PASS fast: JavaScript dependencies
PASS fast: build web app
PASS fast: cargo fmt --check
PASS fast: cargo clippy
PASS fast: cargo test
PASS fast: svelte-check
PASS fast: UI package svelte-check
PASS fast: stream package typecheck
PASS fast: Vitest
PASS fast: UI package Vitest
PASS fast: stream package Vitest
PASS fast: OpenAPI and TypeScript client drift
PASS fast: terminal TypeScript
PASS fast: terminal Vitest
Fresh production browser gate is running now; screenshots and browser output are pending.
The latest production-browser run passes desktop sidebar group reordering but exposes a mobile touch case: dragging Hosts onto Containers in the same group does not resolve Containers as the current drop target. I added browser-only pointer-coordinate and row-order telemetry to capture the event and geometry from the failing run before changing behavior.
The failed mobile touch drag captured the Hosts row already reordered beyond the pointer: the pointer was at y=233, Containers was y=176–208, the dragged Hosts clone was y=225–257, and the source placeholder was y=308–340. The item path was mutating order in both
onDragMoveandonDragOver; I restricted per-move placement to groups, leaving items on the target-changingonDragOverpath, and updateddocs/UI.mdto describe the split.scripts/verify fastprinted all 15 PASS stages after the code change; the production browser case still needs a rerun.After limiting
onDragMoveto groups, the production browser gate still places Hosts at the end during the mobile item drag and loses the Containers drop-target marker. The DOM records the source near y=225–257, Containers at y=176–208 and the original source placeholder at y=308–340. The updated fix therefore needs to account for the target changing as the live list reflows; I added a test-enabled trace of each itemonDragOvertarget, pointer Y, target center and before/after group order. A freshscripts/verify fastafter the instrumentation exited 0 and printed all 15 PASS stages.The item drag trace isolated the missed transition: the last
onDragOverfor the Containers row occurred at pointer y=211.3 with target center y=225, so the item correctly stayed above it. The final move to y=233 remained inside the same row, so dnd-kit emitted no secondonDragOver. I addedonDragMoveplacement for items and groups only while the incoming pointer coordinate remains inside the previous target's current bounds; that handles same-target center crossings and skips stale targets after the pointer enters another row. The helper has focused bounds tests, andscripts/verify fastpassed all 15 stages after the change. Production-browser verification is next.The first browser attempt after commit
3a1afbastill failed before the mobile touch drag settled: the order stayed unchanged and no row had.drop-target. The trace confirmsonDragOverentered Containers at y=211.3, above its y=225 center; the final pointer y=233 stays within that row without changing target ID. I expanded the test-only trace to recordonDragMove, its incoming coordinate, target bounds and in-bounds decision so the next production run can show why same-target placement was not applied. The component check passed with 0 errors/0 warnings, and Vitest passed 11/11.The production browser run passed the mobile touch item reorder and exact group-order assertions, then failed later because the test searched for the group DropdownMenu inside the sidebar dialog. Playwright's accessibility snapshot shows the menu portaled at page level. The check now queries
page.getByRole("menu"), asserts the expected mobile group order, and no longer requires a row to stay the active drop target after live reflow. I removed the temporary DnD tracing. The latestscripts/verify fastexited 0 with all 15 PASS stages; the final browser gate is pending.Finding: the independent review found that pointer/touch sorting could change order in
onDragMovewithout a live position update when the hovered target remained unchanged or reflowed to the dragged row. dnd-kit’s Accessibility plugin documents drag-over callbacks as its default live announcement path and allows a callback to returnundefinedto skip a message: https://dndkit.com/extend/plugins/accessibility/.Fix at
6515016: actual item/group order changes now update the sidebar’s existing live status with the item/group label and resulting position. The plugin’s drag-over message is suppressed so target-only changes do not duplicate or misstate the new position. Browser assertions cover keyboard and mobile touch item/group reorders. The inbox opaque-card/header-tint assertions and fix are also checkpointed at01a499f.Evidence: targeted sidebar Vitest passed (7/7); web Svelte check passed (0 errors, 0 warnings);
scripts/verify fastexited 0:The final browser gate and screenshot review are still pending.
The first production browser rerun after the sidebar announcement fix measured
116940bytes gzip (under the 120 KB budget), captured desktop route/inbox states and passed the topbar/inbox scrolling checks. It then failed during mobileRename group: the portaled menu stayed open and the inline rename textbox was not created. The same click failure reproduces in an isolated production page at both 320px and desktop width; hit testing confirms the visible menu item received the click. I’m tracing whether Bits UI dispatches theonSelectcallback, then will correct the interaction and rerun the browser gate. The full fast gate remains green.This browser run used the committed checkpoint
01a499f; the additional cross-group touch assertion was added afterward and is not included in that run.Root cause found: when either inline form mounted, its
Inputreceivedbind:ref={undefined}even though the shadcn Input ref prop has anullfallback. Svelte raisedprops_invalid_value(Cannot do bind:ref={undefined} when ref has a fallback value): https://svelte.dev/e/props_invalid_value. Both sidebar Input refs now initialize tonull.The browser check now exercises creating a group, renaming and saving Infrastructure, restoring the default arrangement, and asserting a cross-group touch item move announces its destination and position.
scripts/verify fastexited 0 with all 15 stages PASS (exact output was recorded in the earlier issue comment). Focused Svelte check passed with 0 errors/0 warnings and sidebar Vitest passed 7/7. Commit:9d60729. The final browser gate is pending.The next production browser pass fixed and verified the sidebar create/rename issue: the create form focused and submitted, the Infrastructure rename saved, arrangement reset, and a cross-group mobile touch move announced
Hosts moved to Access, position 2 of 2.. The run measured 116,940 bytes gzip. It then stopped at mobile inbox capture because the test tried to activate the sidebar inbox item while the mobile drawer was closed. The browser check now opens the drawer first and checks focus returns to Open navigation after closing the inbox. This test-harness adjustment is committed at96b31f6; final browser rerun and screenshot review are pending.Browser finding at
e8b4415:checkToastTheme()timed out on the 390 px pass becauseAppShell.svelteintentionally hides.theme-toggleat widths up to 767 px. The desktop/tablet control remains available. Updatedscripts/browser-check.mjsto run the toast theme interaction only at widths where the toggle is rendered; the mobile light/dark route captures still run. Rechecking with the full gates now.Browser finding at
b3f669f: the full gate passed the app’s signed-in route captures at 116,941 gzip bytes, thenterminal production first-load budgetmeasured the anonymous setup route at 122,967 bytes against 122,880 (87 bytes over). The production test measures the initial setup page, while the browser-check metric runs after setup/sign-in.AppShell.sveltestatically imports the signed-in Sidebar, Button and Sonner modules even while showing the signed-out auth screen. I’m splitting the authenticated shell behind the existing session restore so the setup route does not load unused workspace UI; then I’ll rerun both gates.Finished step 12e. No push.
Branch:
shadcnBase:
devat7bda001HEAD:
9678ca6ce93add20ae491cde41600e9741a401b9Delivered: initialized shadcn-svelte 1.7.0 with Tailwind CSS 4.3.3, Vite plugin and Bits UI; committed
components.json. Replaced the owned hand-rolled UI primitives with generated shadcn-svelte components, migrated the owned pages, and kept virtualization and keyboard navigation in the shadcn-basedVirtualTable. The app shell uses shadcn Sidebar, Sheet, Dialog, DropdownMenu and Sonner; it supports icon collapse, mobile navigation, keyboard toggling, account footer, inbox and direct item/group reordering. Design tokens remain the source of truth.DnD:
@dnd-kit/svelte+@dnd-kit/helpers0.5.0. The Svelte 5 primitives fit the existing sidebar markup; the package provides pointer, touch and keyboard sensors plus accessibility announcements, and its helpers support sortable items and groups. This avoids a React adapter. References: Svelte quickstart, sortable primitive, provider and sensors, accessibility plugin.SvelteBits was not imported: its MIT plus Commons Clause license conflicts with the repository's AGPL-only decision; this is recorded in
docs/UI.md.First-load JS: pre-migration dashboard baseline was 76,757 gzip bytes. The final production anonymous setup check measured 83,804 of the 122,880-byte (120 KiB) budget, with no terminal resources loaded (
production-first-load.json). The final number is for the setup route after deferring the signed-in shell until session restore. SvelteKit still fetches the current route's page module; the measured production first-load budget passes.Gate output:
Screenshots are under
/mnt/hdd/infraternal/artifacts/shadcn/. Route captures:light-1440x900-*.png,dark-1440x900-*.png,light-390x844-*.png,dark-390x844-*.png. Special captures includesidebar-collapsed-{light,dark}-1440x900.png,sidebar-drag-{light,dark}-1440x900.png, andinbox-open-{light,dark}-{1440x900,390x844}.png. Also captured device approval and account footer states. Representative desktop/mobile, light/dark, auth, host/container, inbox and sidebar screenshots were visually reviewed.Independent review — review-only
Range:
shadcnat9678ca6ce93add20ae491cde41600e9741a401b9, against7bda001e5bd8ff42c3768fb1e6cbba317d22789e.Verdict: Approve. No high-severity finding; both introduced fixes can land later.
Findings
Introduced · LATER — Dashboard status badge loses its visual variant. The Dashboard passes
variant={error ? "warn" : "ok"}atpackages/web/src/routes/+page.svelte:72, but the generated Badge element only forwardsdata-slot="badge"(packages/ui/src/lib/components/ui/badge/badge.svelte:47-53). Its app variant rules requiredata-variant(badge.svelte:60-66). Thelight-1440x900-dashboard.pngcapture shows “Live inventory” as plain text. Fix: forwarddata-variant={variant}or apply the semantic variant classes directly.Introduced · LATER — Sidebar group headings do not share a text inset. In
sidebar-rest-light-1440x900.png, WORKSPACE begins at x=16 while INFRASTRUCTURE, ACCESS and ADMIN begin at x=42. The CSS explains the 26 px difference:.primary-labelhas 8 px inline padding (PrimaryNavGroup.svelte:38); sortable headings follow a 28 px handle, 2 px gap and 4 px title padding (SortableNavGroup.svelte:155-158). Insidebar-drag-dark-1440x900.png, the non-dragged headings stay at x=42; the active ACCESS group moves with the pointer, so the reported shift of the other headings is not reproduced. Fix: give the heading text a shared inset while retaining usable drag/manage targets.Pre-existing · LATER — Dashboard refresh state changes have no live announcement. The “Live inventory” / “Refresh delayed” text is rendered in a plain heading row (
packages/web/src/routes/+page.svelte:71-72), with norole="status"or live region; the existing StatusDot text wrapper also has no live semantics (packages/ui/src/lib/StatusDot.svelte:14-17). The Dashboard status markup predates this change, so this is not a migration regression. Fix: announce refresh-state changes through a polite status region, without repeatedly announcing unchanged refreshes.Requested checks
packages/ui/src/tokens.cssis the token source; shadcn semantic names map to it. No duplicate token system was added. Sampled body, muted and status text/background pairs in both themes meet 4.5:1 (lowest sampled pair: 4.95:1); this was not an exhaustive per-component contrast scan. Emil and thebetter-interface/better-*review rules were applied. Copy, hierarchy, spacing and typography produced no additional finding./measured 115,288 B gzip across 26 scripts, under the 122,880 B budget by 7,592 B; no terminal resources loaded.Gates
scripts/verify fast:scripts/verify browser:Started fix-shadcn on branch shadcn at base
9678ca6ce9. Scope: Badge variants, sidebar group heading alignment, dashboard refresh announcements, and browser assertions.Finding: badge.svelte defines token-backed CSS selectors for neutral, accent, ok, warn, error, info, and offline on data-variant, but the Badge element only emits data-slot; current Badge uses across Dashboard, Containers, Hosts, and Device therefore miss those rules. Decision: preserve the existing shared variant styles by forwarding data-variant from the primitive.
Decision: the sortable group title now precedes its controls in the header flex row, so the label starts at the same 8 px inset as WORKSPACE while the drag and manage buttons retain separate 28 px desktop and 40 px coarse-pointer targets. The baseline browser run captured desktop Dashboard, sidebar-rest and sidebar-drag artifacts under artifacts/fix-shadcn/before.
Finding from the after browser run: heading alignment passes at rest, but the active Access heading moves to x=-55.5 while other headings remain at x=16 when the pointer moves horizontally from the right-side handle to the destination center. Badge and refresh-status assertions passed before this failure. I am correcting the group drag path so the active label also stays on the shared inset.
The final browser run passed the badge background, refresh announcement, and sidebar alignment assertions, including a real vertical group reorder. The gate then reported only two Chromium “Failed to load resource” console diagnostics from the deliberate 503 failure injection. I am keeping the 503 response and exempting only that exact expected diagnostic; application console errors remain checked.
Finished the three review fixes on branch
shadcn.Head:
8462ec4480f380d1fbc1c1b008744dbe707ce030Evidence: the Badge forwards
data-variantand all rendered Badge variants have a computed background distinct from the page background. Workspace and sortable group heading text edges match at rest and during vertical group reordering. The Dashboard uses a stable polite status region; browser assertions confirmed one announcement for theRefresh delayedtransition and one for recovery, without repeating an unchanged state. The production screenshots before and after are under/mnt/hdd/infraternal/artifacts/fix-shadcn/{before,after}/, including Dashboard and sidebar rest/mid-drag at desktop and mobile sizes.scripts/verify fastoutput:scripts/verify browseroutput (started at load5 7.11):The browser check deliberately injects 503 responses to exercise refresh failure. The console assertion ignores only the exact Chromium 503 diagnostic for those injected responses; other console errors remain failures.
Orchestrator: the fix round (
8462ec4) fixed the badge styling and the mid-drag heading alignment. Later, carried into 12g part b (#34): at rest, the "WORKSPACE" heading text starts at x=16 while INFRASTRUCTURE, ACCESS and ADMIN start at x=12 (/mnt/hdd/infraternal/artifacts/fix-shadcn/after/light-1440x900-dashboard.png). The rest-state alignment assertion should measure the heading text left edge, not the label box.Starting mechanical integration on branch
shadcnat base head8462ec4480f380d1fbc1c1b008744dbe707ce030. I will fetchorigin/dev, merge its current head, preserve the container logs/terminal behavior and shadcn primitives, resolve generated files through their generators where needed, then runscripts/verify fastandscripts/verify browserwhen load5 is below 8. No push.The merge exposed conflicts in
docs/UI.md, the UI primitive fixture/index/tests,packages/ui/src/lib/Table.svelte(removed by shadcnVirtualTable, modified on dev), the host detail route, andscripts/browser-check.mjs. The container inventory and detail route use the shadcn table/components; I am preserving that implementation, adding the streamed log-list primitive, and adapting dev's host/stream browser coverage to the shell. Browser resolution preserves both branches' checks, including sidebar pointer/touch/keyboard reorder and accessibility checks.The first
scripts/verify fastattempt reachedFAIL fast: build web app. Vite could not resolve./Badge.svelteimported by the devVirtualLogList; the shadcn branch removed that legacy primitive. I am switching the log stream label to the generated shadcn Badge and cleaning Svelte's scoped CSS warnings, then I will rerunfast.The first
scripts/verify browserrun built the release binary but stopped before the Dashboard matrix at the additional container-detail bundle assertion:METRIC first-load JS gzip before terminal: 219733 bytes, followed byfirst-load JavaScript exceeds 120 KiB gzip: 219733. This assertion was added on dev for the container detail route; the job's 120 KB requirement is specifically the signed-in Dashboard. I will retain the container-detail metric as diagnostic output and leave the strict 120 KB assertion on the existing cold Dashboard measurement, then rerun browser when load5 is below 8.Browser gate finding: the stream filter in the merged log view is the shadcn Select trigger, exposed as a button with accessible name
Stream; the inherited check still queried a nativecomboboxand timed out atscripts/browser-check.mjs:1371. Updated the check to query the accessible button and am rerunningscripts/verify browser. The same run confirmed the container detail diagnostic bundle at 219733 bytes gzip; the required 120 KiB assertion remains on the signed-in Dashboard cold-load check.Browser gate finding: after the stream selector fix, the run passed the log assertions, Ghostty prompt/theme checks, terminal DATA/CREDIT accounting and reattach. The later sign-in step found two
Usernamelabels in the auth screen's setup and sign-in tab panels, so the inheritedgetByLabel("Username")locator was ambiguous. I’m changing this check to the existing#signin-usernameselector used by the setup flow and rerunning the browser gate.Mechanical merge finished on
shadcn; no push.Merge commit:
db5b5cbf19a7c5d2455225c75e3b8216075f76e7Parents:
8462ec4480f380d1fbc1c1b008744dbe707ce030and1337efde31f52d0c717029c1760ff7d3c7eb2940.Resolutions: retained the dev container log stream and Ghostty terminal behavior, rendered through this branch's shadcn components; kept the shadcn shell and drag-and-drop checks; preserved both sides' browser checks. Updated the stream selector and sign-in username locators for the merged UI.
bun installreported no dependency changes. The signed-in Dashboard cold-load assertion disables cache and enforces the 120 KiB gzip budget; the browser gate passed.scripts/verify fastoutput:scripts/verify browseroutput: