Five small design/layout nits from the UI sweep:
- form.selectPlaceholder "— select —" → "Select…" / "Välj…", matching
the affordance style of every other placeholder (Filter…, Search…).
- FieldForm in edit mode now explains its locked controls with a muted
fields.lockedNote caption ("Key and type can't be changed after
creation.") instead of leaving four silently disabled inputs.
- FieldList rows truncate long labels (min-w-0 on the row button +
truncate on the label, shrink-0 on the badge and required marker)
instead of overflowing the 20rem column.
- The sidebar collapse toggle is hidden on narrow viewports (hidden
md:flex) instead of rendered permanently disabled/grayed — the rail
is forced collapsed there anyway.
- PageTitle gains text-balance so long titles wrap evenly.
Closes#73
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
FieldsPage kept the selected field definition in component state, so
reload lost the selection, fields couldn't be linked/shared, and
back/forward didn't navigate selections — inconsistent with
/vocabularies/:id and /objects/:id.
Move selection into the URL: the route becomes /fields/:key?
(optional segment), FieldList selection navigates, cancel/done
navigates back to /fields, and the page derives the selected def from
the already-cached field-defs query. An unknown or stale key (e.g.
after deleting the selected field) falls back to the create form.
Tests: deep link opens the locked edit form, select→cancel round-trips
through the URL, unknown key falls back to create.
Closes#72
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Nothing in the app respected prefers-reduced-motion — the kit's
data-open/closed animations, the skeleton pulse, and the sidebar width
transition all ran unconditionally. Add a global base-layer rule that
collapses animation/transition durations to a single frame when the OS
asks for reduced motion; one rule covers current and future additions.
Add overscroll-y-contain to the scrollable modal/popup surfaces
(DrawerContent, SelectContent, ComboboxPopup) so flicking past the end
of their content no longer chain-scrolls the page beneath, and to
AlertDialogContent for when it gains scrollable content.
Verified in the built CSS: the media query and
overscroll-behavior-y:contain both compile into dist/assets.
Closes#71
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The delete dialogs (DeleteObjectDialog and the shared
DeleteConfirmDialog) left their confirm button enabled during the
in-flight request, so a double-click fired a second DELETE that 404'd
and surfaced a spurious error. Disable cancel + confirm while pending
and swap the confirm label to a new actions.deleting ("Deleting…" /
"Tar bort…").
The login button disabled itself during login.isPending but kept the
"Sign in" label; it now shows auth.signingIn ("Signing in…" /
"Loggar in…") so slow networks get visible feedback.
Each fix is covered by a gated-MSW (or gated-promise) test asserting
the pending label + disabled state before releasing the request.
Closes#70
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keyboard focus was invisible on the objects-table sort headers and
page-size select, breadcrumb links, the external-URI link, and the
combobox input/clear/trigger. Apply the shared focusRing helper in app
code and the kit's inline focus-visible classes (matching input.tsx)
in ui/combobox.
Make the search result count a role="status" live region so screen
readers announce updated counts while typing; the existing search test
now asserts the count through getByRole("status").
Closes#69
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Tooltip, toast, and combobox popups still hardcoded light colors
(bg-white, neutral-*, indigo-50) and rendered as white boxes in dark
mode; the objects-table page-size select did the same in app code.
Swap all of them to theme tokens (popover/accent/muted/destructive/
success) and replace the toast's literal "×" with the lucide X icon.
Wire browser chrome into the theme: color-scheme via CSS on
:root/.dark (follows the in-app toggle, not just the OS), a
theme-color meta kept in sync by the preload script and applyTheme(),
plus a unit test for the meta sync.
Extend check-no-raw-colors to also flag shadeless white/black
utilities outside components/ui/ so the objects-table case can't
recur.
Closes#68
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add shared formatTimestamp(value, timeZone, locale) helper — date + short
time in the instance tz/locale, with a UTC fallback on an invalid IANA zone.
Route the objects-table 'Updated' column through it (was inline, date-only,
unguarded). Display-only; storage stays UTC.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Same timing-window race as object-new-page: the 50ms delay let the logout
resolve (menu unmounts on me=null) before findByText caught 'Signing out…'
on the slow CI runner. Hold the logout open with a promise released only
after the pending state is asserted.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The 'narrow: detail renders inside a portaled drawer' test lazy-loads the
drawer chunk and exceeded the 5s default on the slow CI container (setup
alone took ~486s). Bump testTimeout on both vitest projects.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- CI runs the @vitest/browser-playwright (storybook) project, which needs the
chromium browser downloaded — add 'playwright install --with-deps chromium'.
- object-new-page in-flight test held the create mutation open with a 50ms
delay and raced the pending-state assertion (failed under CI timing); gate it
on a promise released only after asserting 'saving…', so it's deterministic.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds web/GUARDRAILS.md capturing the recurring CI-guardrail and
test-harness lessons in one place: the check:size (250 KB-gz largest
chunk) and check:colors (design-token) guards, the jsdom/storybook
vitest split, MSW onUnhandledRequest:"error" overrides, RTL
accessible-name collisions, Storybook nested-router/portal handling,
and the components/ui code-style split. Wires a pointer from CLAUDE.md.
All claims verified against the live scripts, ci.yaml, and src/test/.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Convert app.tsx route tree verbatim to a module-level data router via
createRoutesFromElements + RouterProvider, and the test harness to
createMemoryRouter + RouterProvider. The search NavLink-click test now mounts
its routes as real data-router routes so RouterProvider intercepts the link
(descendant <Routes> under a catch-all let it fall through to a jsdom navigation).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- **Code navigation:** prefer the insikt LSP server over grep/glob — it resolves macro-generated symbols that text search misses. (insikt runs standalone, not via the gateway MCP.)
- **Code navigation:** prefer the insikt LSP server over grep/glob — it resolves macro-generated symbols that text search misses. (insikt runs standalone, not via the gateway MCP.)
- **Dependencies:** manage via the `cargo-mcp` server rather than editing `Cargo.toml` by hand.
- **Dependencies:** manage via the `cargo-mcp` server rather than editing `Cargo.toml` by hand.
- **Frontend guardrails:** before touching `web/`, read **[web/GUARDRAILS.md](web/GUARDRAILS.md)** — it covers the CI gate (`check:size` 250 KB-gz budget, `check:colors` design-token enforcement) and the test-harness quirks (MSW `onUnhandledRequest: "error"`, the jsdom/storybook vitest split, RTL accessible-name collisions, Storybook nested-router and portal handling, and the `components/ui/` code-style split).
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Fill the empty app header with wayfinding — a route-driven breadcrumb (left), a signed-in user menu + compact global search (right) — and render the configured `app_name` for the brand + login.
**Architecture:** A page-driven breadcrumb (a `BreadcrumbProvider` context + `useBreadcrumb(trail)` hook, parallel to #57's `useDocumentTitle`) that each route sets and the header renders. A reusable `ui/menu.tsx` Base UI Menu wrapper powers a `UserMenu` (email/role + Sign out). A `HeaderSearch` input navigates to `/search?q=`. Brand + login read `useConfig().app_name`. No new dependency.
- [ ]**Step 1: Sidebar brand.** In `web/src/shell/sidebar.tsx` add `import { useConfig } from "../config/config-context";`, get `const { app_name } = useConfig();` in the component, and change line ~76:
- [ ]**Step 2: Login.** In `web/src/auth/login-page.tsx`: add `import { useConfig } from "../config/config-context";`, `const { app_name } = useConfig();`. Change the `<h1>` (line ~38) to `{app_name}` and the title effect (line ~18) to `document.title = app_name;` with deps `[app_name]`. Remove the now-unused `t` for that purpose only if `t` is otherwise unused (check — login uses `t` for field labels/errors, so keep the `useTranslation` import).
- [ ]**Step 3: Remove the dead i18n key.** Delete the `"app": { "name": "..." }` entry from BOTH `web/src/i18n/en.json` and `web/src/i18n/sv.json` (grep first: `grep -rn 'app\.name\|"app"' web/src` — confirm no remaining `t("app.name")` after Steps 1–2). en/sv must stay in parity (remove from both).
- [ ]**Step 4: Update login test if needed.** Read `web/src/auth/login-page.test.tsx`. If it asserts the heading text via `t("app.name")` / "Collection", update it to the config default `"Collection Management System"` (the value `useConfig` returns in tests via `DEFAULTS`). Do NOT weaken; just match the new source.
- [ ]**Step 5: Verify (run vitest once for these files).**
`cd web && pnpm vitest run src/auth src/shell/app-shell.test.tsx && pnpm typecheck && pnpm lint`
Expected: PASS. The sidebar brand + login now show "Collection Management System" (config default) in tests.
- [ ]**Step 1: Read the reference**`web/src/components/ui/alert-dialog.tsx` for the exact house pattern (namespace import, `data-slot`, `cn()`, no semicolons, token classes). The Base UI Menu API is `import { Menu } from "@base-ui/react/menu"` then `Menu.Root`, `Menu.Trigger`, `Menu.Portal`, `Menu.Positioner`, `Menu.Popup`, `Menu.Item`, `Menu.Separator`. **This is novel — you MUST validate the exact part tree by running the story (Step 3).**
- [ ]**Step 2: Implement**`web/src/components/ui/menu.tsx` (no-semicolon style). Export: `Menu` (Root re-export with data-slot), `MenuTrigger`, `MenuContent` (composes Portal + Positioner + Popup), `MenuItem`, `MenuSeparator`. Skeleton (adapt class/props to what runs):
IMPORTANT: the exact prop names (`sideOffset`, `align`, `Popup` vs `Popup`+`Positioner` arrangement) MUST be confirmed against the installed `@base-ui/react` types — open `web/node_modules/@base-ui/react/menu/` or check via the editor/types and adjust. Do not guess; if a prop/part errors at typecheck or runtime, fix it to match the real API. No `data-[highlighted]` raw colors — `bg-accent`/`text-accent-foreground` are tokens (OK).
- [ ]**Step 3: Story**`web/src/components/ui/menu.stories.tsx` (single-quote, no-semicolon). Render a `Menu` with a `MenuTrigger` (a Button via `render` or as child) + `MenuContent` with two `MenuItem`s; a `play` test that opens the menu (click the trigger) and asserts an item is visible:
If `MenuTrigger render={<Button/>}` isn't the right composition for Base UI Menu, use the pattern that works (e.g. `<MenuTrigger><Button/></MenuTrigger>` or `render` per the alert-dialog usage). The story passing IS the validation.
- [ ]**Step 4: Run the story-as-test + typecheck + lint.**
`cd web && pnpm vitest run src/components/ui/menu.stories.tsx && pnpm typecheck && pnpm lint`
Expected: PASS. If the menu doesn't open / portal isn't found, fix the part tree until the play test passes (this is the validate-by-running step). The portal renders to document.body — `findByText` on the canvas/body should find it; if the addon's `canvas` is scoped, query `within(document.body)` or use the screen — match how other portal-using stories (drawer/combobox/toast) assert.