Panels
- React + Vite + TypeScript + Tailwind 4 + shadcn/ui (style
radix-nova, base color neutral, lucide
icons), pnpm. The same components.json everywhere, with the @xvii registry added
(see services/ui.md).
- Components come only from
@xvii (ADR 0029): its own items in src/components/xvii and the shadcn
primitives it vendors in src/components/ui and src/hooks/use-mobile.ts. These files are
generated: don't edit them in a service; change them in ui, then pnpm xvii:sync. A primitive the
registry lacks is vendored into ui first. Tailwind classes only for layout; no custom CSS.
- Every panel uses the
@xvii/app-shell: sidebar with the service's sections, service switcher, team
switcher, account menu, ⌘K command menu, theme and language toggles. Header actions are Action[]
(an ActionBar folds what doesn't fit into a ⋯ menu); other header controls go in headerExtra.
Routes are paths; the binary serves index.html for unknown paths.
- Readable surfaces: tables, mobile list rows and standalone content panels use
bg-card and
text-card-foreground, not a border alone. The shared brand applies tinted, blurred glass when
wallpaper is set; only the page canvas is transparent. Keep nested detail tables on their enclosing
card or dialog surface instead of adding another glass layer. Standalone inputs, input groups,
textareas and select triggers also keep a glass backing; grouped controls stay transparent inside
their group so icons, placeholders and values share one readable surface.
- Data: TanStack Query over the service API. With a contract, the client is generated from it
(
src/lib/api.gen.ts, make api, ADR 0027) and src/lib/api.ts keeps only hand-written helpers;
without one, it is typed by hand in src/lib/api.ts (one place per service).
- i18n:
src/i18n/en.ts is the source of truth, ru.ts is typed by it so a missing key fails the
build. Strings through useI18n().t, dates and sizes through useFormat(). Language: saved choice,
then the browser.
- Sign-in: the panel never shows a login form. No session → redirect to
id (OIDC code + PKCE) through
the service's /auth/login; @xvii/auth handles it.
Phones and tablets
Every screen works at 360, 390, 820, 1180 and 1440 px, by touch on phones and tablets, in English and
Russian (ADR 0030). Design spec: superpowers/specs/2026-10-21-registry-mobile-design.md. The
registry APIs named here arrive with the @xvii phones release; a panel not yet migrated meets the
same outcomes with what it has.
- Nothing exists only on hover, in a tooltip, in a hidden column or behind a sideways scroll without a
hint. Everything a screen offers at 1440 px is reachable at 360 px.
- Tables:
DataTable with a label. Row actions go through rowActions (usually a RowMenu): the
table renders them as a sticky last column and at the end of every phone item; never an actions
column of your own, never buttons inside renderMobile. A row that opens a view has rowHref and
RowLink. Pass stable arrays (memoize): a new array per render loops TanStack Table.
- Columns hide by the table's own width:
meta.priority 2 or 3, never max-lg:hidden and the like;
what a hidden column shows must be in the row's detail or phone item. One column grows; columns
with unbounded text set meta.overflow.
- Phone items:
renderMobile returns content only (no buttons or links), or leave it out and set
meta.mobile on the columns. Filters go in filters (a drawer on phones).
- Detail views are a
DetailSheet (size preset, header with actions, pinned tabs and footer); forms are
a ResponsiveDialog (a drawer on phones) with the submit in its pinned footer.
- Information that used to be a tooltip is a
Hint; times are Time; long values are Truncate;
copying is useCopy; anything wider than its box scrolls inside a ScrollHint.
- Targets are 40 × 40 px on a coarse pointer (the vendored primitives do it). Don't shrink controls
with
size-* or h-*; a control of your own gets pointer-coarse:min-h-10 pointer-coarse:min-w-10.
Hover-revealed controls use pointer-fine: variants so they are always visible on touch.
- Every control has an accessible name in the panel's language; clickable things are buttons or links.
index.html has <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover, interactive-widget=resizes-content">. Panels ship no manifest or service
worker: the installable app is the hub at <d>/app, which the account menu links to.
Errors
main.tsx calls installErrorReporting() and wraps the app in the @xvii/error-boundary
ErrorBoundary; the shell wraps each page in another, and the panel calls setReportUser once /me
answers. Reports go to the instance's errors, to the platform project whose DSN home's catalog hands
out, with the release and nothing more than ADR 0031 allows.
Checks
pnpm lint, pnpm build, pnpm xvii:check (generated files equal the registry) and
pnpm test:layout pass. The layout test lists every screen in tests/layout/panel.layout.ts with
synthetic fixtures (example.com, alice, long and Russian values, every state); exceptions carry a
reason. CI runs all four. The build output is embedded, so a service is rebuilt after panel changes.