diff --git a/.gitignore b/.gitignore index 4cafb5599..c030c35b7 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,4 @@ data/ .direnv/ result .superpowers/ +test-results/ diff --git a/notes/story-117-ux-consistency/design.md b/notes/story-117-ux-consistency/design.md new file mode 100644 index 000000000..036e9d8af --- /dev/null +++ b/notes/story-117-ux-consistency/design.md @@ -0,0 +1,157 @@ +# Story #117: Resolve UX Inconsistencies — Design + +## Goal + +Unify navigation patterns across the application so users always know where they are and how to get back. Anchor the design on the GitHub repo-as-container model: every meaningful action happens within a project context. + +## Approach + +Repo-centric navigation (Approach B from brainstorming). Move forms under project scope, add a global projects directory, unify breadcrumbs on one component, and add a "Forms" tab to the existing repo nav. + +## Design + +### 1. Route Restructuring + +Forms move under project scope. The project is the universal container. + +| Current Route | New Route | Notes | +|---------------|-----------|-------| +| `GET /forms` | `GET /forms` | Stays — becomes cross-project form directory | +| `GET /forms/sessions` | `GET /forms/sessions` | Stays — "my sessions" across all projects | +| `GET /forms/:specId` | `GET /:owner/:slug/forms` | Form landing page, scoped to project | +| `POST /forms/:specId/sessions` | `POST /:owner/:slug/forms/sessions` | Create session | +| `GET /forms/:specId/sessions/:sid/pages/:p` | `GET /:owner/:slug/forms/sessions/:sid/pages/:p` | Form page | +| `POST /forms/:specId/sessions/:sid/pages/:p` | `POST /:owner/:slug/forms/sessions/:sid/pages/:p` | Submit page | +| `GET /forms/:specId/sessions/:sid/review` | `GET /:owner/:slug/forms/sessions/:sid/review` | Review | +| `POST /forms/:specId/sessions/:sid/submit` | `POST /:owner/:slug/forms/sessions/:sid/submit` | Submit form | +| `GET /forms/:specId/sessions/:sid/confirmation` | `GET /:owner/:slug/forms/sessions/:sid/confirmation` | Confirmation | +| `GET /forms/:specId/submissions/:subId/pdf` | `GET /:owner/:slug/forms/submissions/:subId/pdf` | PDF download | +| `GET /forms/sessions/:sid/submission` | `GET /forms/sessions/:sid/submission` | Read-only submission (stays top-level) | + +**Branch-qualified variants** follow the same pattern with `/branches/:branch/` inserted after `/forms/`: +- `GET /:owner/:slug/forms/branches/:branch/sessions/:sid/pages/:p` +- etc. + +**Chat routes** follow the same pattern with `/chat` appended to page routes. + +**Key decisions:** +- `:specId` drops from URLs — a project has one form spec (`forms/default/`), so `/:owner/:slug/forms` is unambiguous. +- The `specIdIndex` cache continues resolving `specId → (owner, slug)` internally. No schema migration for sessions. +- No redirects from old URLs — clean break (this is a lab, not production). + +### 2. Breadcrumb Model + +**One component everywhere.** The existing `flex-breadcrumb` design system component replaces all custom breadcrumb HTML. The custom implementation in `owner/components.tsx` gets removed. + +**Breadcrumb patterns by context:** + +``` +Project browsing: + daniel / snap-application / tree / main / src + daniel / snap-application / commits + daniel / snap-application / settings + +Form filling (under project): + daniel / snap-application / forms / Page 2 of 5 + daniel / snap-application / forms / Review + daniel / snap-application / forms / Confirmation + +Form directory (top-level): + Forms + +My sessions (top-level): + Forms / My Sessions + +Catalog: + Catalog / Architecture / Software Architecture + Catalog / Stories / #117 Resolve UX inconsistencies + +Projects directory: + Projects + +User profile: + daniel +``` + +**Rules:** +- Every page except the home page has a breadcrumb. +- When there are multiple segments, every segment except the last is a link. +- The last segment is the current page (not a link). +- Single-segment breadcrumbs (e.g., just "Projects") serve as a page title indicator — not a link. +- Project-scoped pages always start with `owner / project-slug`. + +### 3. Global Projects Directory + +**New route: `GET /projects`** — simple listing of all projects on the instance. + +**Content:** +- All projects with status "ready". +- Each entry shows: project name (linked to project overview), owner (linked to `/:owner` profile). +- Flat list sorted by most recently updated. +- No search, filtering, or pagination. + +**Header nav update:** +- Current: `Home | Forms | Projects (→ /:user) | Catalog` +- New: `Home | Forms | Projects (→ /projects) | Catalog` +- Dashboard (`/`) still shows your recent projects as quick-access. +- User profile (`/:owner`) still shows that user's projects, reachable from the directory. + +### 4. Repo Nav "Forms" Tab + +The existing `RepoNav` component gains a "Forms" tab: + +``` +Overview | Forms | Pull Requests | History | Files +``` + +- Links to `/:owner/:slug/forms`. +- Shows as `current` on all form-filling pages within that project. +- Form-filling pages render `RepoNav` so users can always jump between project sections. +- The `RepoTab` type adds `'forms'` to its union. + +### 5. Header Nav "Current" Indicator + +Replace path-based prefix matching with an explicit `currentSection` prop on Layout: + +- `"home"` — dashboard +- `"forms"` — `/forms` directory and all project-scoped form-filling pages +- `"projects"` — `/projects` directory, `/:owner`, `/:owner/:slug`, and all project browsing +- `"catalog"` — `/catalog/*` + +Each route sets this explicitly when rendering the layout. + +### 6. `/forms` Directory Page + +The top-level `/forms` route changes from a standalone listing to a cross-project directory: + +- Lists all available forms across all projects. +- Each entry shows: form title, project name + owner (linked to project), and a "Start" action linking to `/:owner/:slug/forms`. +- Links go into project-scoped URLs, establishing project context immediately. + +### 7. "My Sessions" Page + +`/forms/sessions` stays top-level since sessions span projects. Each session entry shows: + +- Form title +- Project context (owner/slug, linked) +- Session status and date +- Link into the project-scoped session URL (`/:owner/:slug/forms/sessions/:sid/...`) + +## Out of Scope + +- **Component contract completeness** (AC 3) — separate story for writing `meta.ts`/`contract.tsx` for the 24 unregistered components. +- **Sidebar nav for project pages** — only catalog keeps its sidebar. +- **Search or filtering** on directories. +- **Form step indicator redesign** — current step indicator in FormPageView stays as-is. +- **Redirects from old `/forms/:specId/...` URLs** — clean break. +- **`flex-in-page-nav`** — exists unused, leave it for now. +- **Form edit routes** (`/:owner/:slug/edit/...`) — already project-scoped, no changes needed. + +## Acceptance Criteria Mapping + +| AC | Addressed By | +|----|-------------| +| All navigable pages include consistent breadcrumb trails | Section 2: unified breadcrumb component on every page | +| Navigation patterns are unified | Sections 1, 3, 4, 5: repo-centric model, projects directory, repo tabs, consistent current indicator | +| Design system components have complete contracts | Out of scope — separate story | +| Walkthrough shows no jarring transitions | Sections 1-6 together: consistent chrome from landing → project → form → back | diff --git a/notes/story-117-ux-consistency/plan.md b/notes/story-117-ux-consistency/plan.md new file mode 100644 index 000000000..07c2e653d --- /dev/null +++ b/notes/story-117-ux-consistency/plan.md @@ -0,0 +1,1231 @@ +# UX Consistency Implementation Plan + +> **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:** Unify navigation across the application by moving forms under project scope, adding a global projects directory, unifying breadcrumbs on one component, and adding a "Forms" tab to the repo nav. + +**Architecture:** Refactor the form router to accept owner/slug context instead of specId in URLs. Mount project-scoped form routes before the owner catch-all in server.tsx. Convert the top-level `/forms` route to a cross-project directory. Replace all custom breadcrumb HTML with the `flex-breadcrumb` design system component. Add `currentSection` prop to Layout to replace path-based nav highlighting. + +**Tech Stack:** Hono (server-rendered JSX), Bun, bun:test + +--- + +### Task 1: Add `listAllProjects` to Project Service + +Add a method to list all projects regardless of owner. The store already supports `list()` with no args — we just need to expose it through the service. + +**Files:** +- Modify: `src/services/projects/project-service.ts:69-87` (interface + implementation) +- Modify: `src/services/projects/index.ts` (already exports ProjectService type) +- Test: `test/projects/project-service.test.ts` + +- [ ] **Step 1: Write the failing test** + +Find the existing project service test file (or create one). Add a test: + +```typescript +import { describe, expect, it } from 'bun:test' + +describe('ProjectService.listAllProjects', () => { + it('returns projects from all users', async () => { + // Use the existing test setup pattern from the file. + // Create projects for two different users, then call listAllProjects(). + const all = service.listAllProjects() + expect(all.length).toBeGreaterThanOrEqual(2) + const owners = new Set(all.map(p => p.createdBy)) + expect(owners.size).toBeGreaterThanOrEqual(2) + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test test/projects/project-service.test.ts` +Expected: FAIL — `listAllProjects` is not a function. + +- [ ] **Step 3: Add `listAllProjects` to the `ProjectService` interface** + +In `src/services/projects/project-service.ts`, add to the interface at line ~87: + +```typescript +listAllProjects(): ProjectIndex[] +``` + +- [ ] **Step 4: Implement `listAllProjects` in the factory** + +In the returned object from `createProjectService`, add: + +```typescript +listAllProjects(): ProjectIndex[] { + return store.list() +}, +``` + +This calls `store.list()` with no userId arg, which returns all projects (the SQL is `SELECT * FROM projects ORDER BY created_at DESC`). + +- [ ] **Step 5: Run test to verify it passes** + +Run: `bun test test/projects/project-service.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/services/projects/project-service.ts test/projects/ +git commit -m "feat(projects): add listAllProjects to project service" +``` + +--- + +### Task 2: Add `currentSection` Prop to Layout + +Replace the `currentPath` string prop with a `currentSection` union prop so header nav highlighting doesn't depend on URL prefix matching. + +**Files:** +- Modify: `src/design-system/components/flex-layout/index.tsx:12-18` (props) and lines ~82-123 (nav items) +- Test: `test/design-system/flex-layout.test.ts` (or existing test file) + +- [ ] **Step 1: Write the failing test** + +```typescript +import { describe, expect, it } from 'bun:test' +import { Layout } from '../../src/design-system/components/flex-layout' + +describe('Layout currentSection', () => { + it('marks Forms nav item as current when currentSection is "forms"', () => { + const html = ( + +

content

+
+ ).toString() + // The Forms nav link should have aria-current + expect(html).toContain('aria-current="page"') + // Verify it's the Forms link, not another one + expect(html).toMatch(/Forms.*aria-current|aria-current.*Forms/) + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test test/design-system/flex-layout.test.ts` +Expected: FAIL — `currentSection` is not a recognized prop. + +- [ ] **Step 3: Update the LayoutProps interface** + +In `src/design-system/components/flex-layout/index.tsx`, change the props: + +```typescript +interface LayoutProps { + title?: string + sidebar?: Child + currentSection?: 'home' | 'forms' | 'projects' | 'catalog' + /** @deprecated Use currentSection instead */ + currentPath?: string + user?: HeaderUser | null + contentWidth?: 'centered' | 'full' +} +``` + +Keep `currentPath` temporarily so existing call sites don't break. It will be removed after all callers are migrated. + +- [ ] **Step 4: Update the nav highlighting logic** + +Replace the `current` prop logic in the header nav items (lines ~82-123). The new logic: + +```tsx +const section = props.currentSection +// Fall back to currentPath matching during migration +const isHome = section === 'home' || (!section && props.currentPath === '/') +const isForms = section === 'forms' || (!section && props.currentPath?.startsWith('/forms')) +const isProjects = section === 'projects' || (!section && false) +const isCatalog = section === 'catalog' || (!section && props.currentPath?.startsWith('/catalog')) +``` + +Then use these booleans as the `current` values on each `HeaderNavItem`. + +For the "Projects" link: change `href` from `resolveUrl(\`/\${props.user.login}\`)` to `resolveUrl('/projects')` and use `isProjects` for `current`. + +- [ ] **Step 5: Run test to verify it passes** + +Run: `bun test test/design-system/flex-layout.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/design-system/components/flex-layout/index.tsx test/design-system/ +git commit -m "feat(layout): add currentSection prop for explicit nav highlighting" +``` + +--- + +### Task 3: Add "Forms" Tab to RepoNav + +Add a "Forms" entry to the repo tab navigation. Update the `RepoTab` type and the `RepoNav` component. + +**Files:** +- Modify: `src/entrypoints/app/routes/owner/components.tsx:372-402` (RepoTab type + RepoNav component) +- Test: `test/owner/repo-nav.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +import { describe, expect, it } from 'bun:test' +import { renderRepoNav } from './helpers' // or inline + +describe('RepoNav', () => { + it('renders a Forms tab linking to /:owner/:slug/forms', () => { + // Render RepoNav with current='forms' + // Assert it contains a link to /alice/my-project/forms + // Assert the Forms link has aria-current="page" + }) +}) +``` + +Note: `RepoNav` is not exported — it's a private component inside `components.tsx`. The test should either: +- Test via a route test (render the full page and check the nav), or +- Export `RepoNav` for testing. + +Prefer testing via the route: render `GET /:owner/:slug` and verify the response contains a "Forms" link. + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test test/owner/repo-nav.test.ts` +Expected: FAIL — no "Forms" link in the rendered page. + +- [ ] **Step 3: Update `RepoTab` type** + +In `src/entrypoints/app/routes/owner/components.tsx` line 372: + +```typescript +type RepoTab = 'overview' | 'forms' | 'pulls' | 'history' | 'files' +``` + +- [ ] **Step 4: Add "Forms" tab to `RepoNav`** + +In the tabs array (line 380-385), add the Forms entry after Overview: + +```typescript +const tabs: { id: RepoTab; label: string; href: string }[] = [ + { id: 'overview', label: 'Overview', href: base }, + { id: 'forms', label: 'Forms', href: `${base}/forms` }, + { id: 'pulls', label: 'Pull Requests', href: `${base}/pulls` }, + { id: 'history', label: 'History', href: `${base}/commits` }, + { id: 'files', label: 'Files', href: `${base}/tree/main` }, +] +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `bun test test/owner/repo-nav.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/entrypoints/app/routes/owner/components.tsx test/owner/ +git commit -m "feat(nav): add Forms tab to repo navigation" +``` + +--- + +### Task 4: Create Project-Scoped Form Router + +Refactor `createFormRouter` so it can work in a project-scoped context where `owner` and `slug` come from route params instead of looking up specId. The key change: `formPathPrefix` uses `/:owner/:slug/forms` instead of `/forms/:specId`. + +**Files:** +- Modify: `src/entrypoints/app/routes/forms/index.tsx` — add owner/slug context support +- Test: `test/forms/project-scoped-routes.test.ts` + +- [ ] **Step 1: Write the failing test for project-scoped form landing** + +Create `test/forms/project-scoped-routes.test.ts`: + +```typescript +import { describe, expect, it } from 'bun:test' +import { Hono } from 'hono' +import { createFormRouter } from '../../src/entrypoints/app/routes/forms' + +const TEST_USER = { login: 'alice', name: 'Alice', avatarUrl: '' } + +// Same test fixtures as test/forms/routes.test.ts — reuse the spec registry pattern. + +function createProjectScopedApp() { + const app = new Hono() + app.use('*', async (c, next) => { + c.set('user', TEST_USER) + await next() + }) + app.route( + '/:owner/:slug/forms', + createFormRouter({ + sessionGateway, + submissionGateway, + getSpecs: async (specId) => specRegistry.get(specId) ?? null, + listSpecs: async () => [...specRegistry.values()], + projectContext: { owner: 'alice', slug: 'my-project' }, + }), + ) + return app +} + +describe('project-scoped form routes', () => { + it('GET /:owner/:slug/forms returns form landing', async () => { + const app = createProjectScopedApp() + const res = await app.request('/alice/my-project/forms') + expect(res.status).toBe(200) + const html = await res.text() + expect(html).toContain('Start now') + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test test/forms/project-scoped-routes.test.ts` +Expected: FAIL — `projectContext` is not a recognized option. + +- [ ] **Step 3: Add `projectContext` to `FormRouterDeps`** + +In `src/entrypoints/app/routes/forms/index.tsx`, extend the deps interface: + +```typescript +export interface FormRouterDeps { + // ... existing fields ... + /** When set, the router operates in project-scoped mode. URLs use + * /:owner/:slug/forms instead of /forms/:specId. */ + projectContext?: { + owner: string + slug: string + getOwnerSlug: (c: Context) => { owner: string; slug: string } + } | undefined +} +``` + +Actually, since the router is mounted at `/:owner/:slug/forms`, the owner/slug come from route params. Better approach: add a `resolveProjectContext` function to deps: + +```typescript +export interface FormRouterDeps { + // ... existing fields ... + /** Resolves owner/slug from route context. When provided, routes use + * project-scoped URL shapes (/:owner/:slug/forms/...). */ + resolveOwnerSlug?: (c: Context) => { owner: string; slug: string } +} +``` + +- [ ] **Step 4: Update `formPathPrefix` to support project-scoped mode** + +Add a new helper alongside the existing one: + +```typescript +function projectFormPathPrefix( + owner: string, + slug: string, + branch: string, +): string { + const base = `/${owner}/${slug}/forms` + return branch === MAIN_BRANCH ? base : `${base}/branches/${branch}` +} +``` + +- [ ] **Step 5: Update handlers to use project context when available** + +In each handler (handleLanding, handleCreateSession, handleRenderPage, etc.), detect project-scoped mode: + +```typescript +async function handleLanding(c: Context) { + const branch = readBranch(c) + const specId = c.req.param('specId') + + // In project-scoped mode, resolve specs differently + let specs: ResolvedSpecs | null + let prefix: string + + if (resolveOwnerSlug) { + const { owner, slug } = resolveOwnerSlug(c) + // In project mode, there's one spec per project — use the first from listSpecs + const allSpecs = await listSpecs() + specs = allSpecs[0] ?? null + if (!specs) return c.notFound() + prefix = projectFormPathPrefix(owner, slug, branch) + } else { + if (!specId) return c.notFound() + specs = await getSpecs(specId, branch) + if (!specs) return c.notFound() + prefix = formPathPrefix(specs.dataSpec.id, branch) + } + // ... rest of handler uses `specs` and `prefix` ... +} +``` + +This is repetitive across many handlers. Extract a helper: + +```typescript +async function resolveFormContext(c: Context): Promise<{ + specs: ResolvedSpecs + prefix: string + branch: string + owner?: string + slug?: string +} | null> { + const branch = readBranch(c) + if (resolveOwnerSlug) { + const { owner, slug } = resolveOwnerSlug(c) + const specs = await getSpecs(c.req.param('specId') ?? '', branch) + ?? (await listSpecs())[0] + ?? null + if (!specs) return null + return { + specs, + prefix: projectFormPathPrefix(owner, slug, branch), + branch, + owner, + slug, + } + } + const specId = c.req.param('specId') + if (!specId) return null + const specs = await getSpecs(specId, branch) + if (!specs) return null + return { + specs, + prefix: formPathPrefix(specs.dataSpec.id, branch), + branch, + } +} +``` + +Then each handler becomes: + +```typescript +async function handleLanding(c: Context) { + const ctx = await resolveFormContext(c) + if (!ctx) return c.notFound() + const { specs, prefix, branch } = ctx + // ... render as before using specs and prefix ... +} +``` + +- [ ] **Step 6: Register project-scoped route patterns** + +When `resolveOwnerSlug` is provided, the router is mounted at `/:owner/:slug/forms`, so routes within it are relative: + +```typescript +// Project-scoped: no :specId needed (project has one spec) +if (resolveOwnerSlug) { + forms.get('/', handleLanding) + forms.get('/branches/:branch', handleLanding) + forms.post('/sessions', handleCreateSession) + forms.post('/branches/:branch/sessions', handleCreateSession) + forms.get('/sessions/:sessionId/pages/:pageIndex', handleRenderPage) + forms.get('/branches/:branch/sessions/:sessionId/pages/:pageIndex', handleRenderPage) + // ... etc for all routes +} else { + // Legacy specId-based routes (existing code) + forms.get('/:specId', handleLanding) + // ... etc +} +``` + +- [ ] **Step 7: Run tests to verify everything passes** + +Run: `bun test test/forms/project-scoped-routes.test.ts` +Expected: PASS + +Also run existing tests to ensure no regression: +Run: `bun test test/forms/routes.test.ts` +Expected: PASS (existing specId routes still work) + +- [ ] **Step 8: Commit** + +```bash +git add src/entrypoints/app/routes/forms/index.tsx test/forms/project-scoped-routes.test.ts +git commit -m "feat(forms): support project-scoped form routing" +``` + +--- + +### Task 5: Mount Project-Scoped Form Routes in server.tsx + +Wire up the new project-scoped form router in the server, mounting it before the owner catch-all route. Update the top-level `/forms` to be a directory page. + +**Files:** +- Modify: `src/entrypoints/app/server.tsx:466-569` +- Test: `test/forms/directory.test.ts` + +- [ ] **Step 1: Write the failing test for project-scoped mounting** + +```typescript +describe('project-scoped form routes via server', () => { + it('GET /:owner/:slug/forms serves the form landing', async () => { + // Use the full app from server.tsx or a test harness + const res = await app.request('/alice/my-project/forms') + expect(res.status).toBe(200) + }) +}) +``` + +- [ ] **Step 2: Create a project-scoped form router in server.tsx** + +Between the existing `/forms` mount and the owner catch-all, add: + +```typescript +// Mount project-scoped form routes BEFORE owner catch-all +const projectFormRouter = new Hono() +projectFormRouter.route( + '/:owner/:slug/forms', + createFormRouter({ + sessionGateway, + submissionGateway, + conversationGateway, + fillingAgent, + specSnapshotStore, + resolveOwnerSlug: (c) => ({ + owner: c.req.param('owner')!, + slug: c.req.param('slug')!, + }), + async getSpecs(specId, ref) { + // This will be called with empty specId in project mode; + // we need to resolve from the project context. + // The resolveOwnerSlug function provides owner/slug. + // We'll look up the project's spec directly. + const project = await findProjectBySpecId(specId) + if (!project) return null + return readProjectSpecs(project.slug, ref ?? 'main') + }, + async listSpecs() { + // Same as before — returns all specs + // ... (reuse existing listSpecs logic) + }, + getEditHref(specId, branch) { + const entry = specIdIndex.get(specId) + if (!entry) return null + return resolveUrl(`/${entry.owner}/${entry.slug}/edit/${branch}`) + }, + async getSourcePdf(specId, _specVersion) { + const project = await findProjectBySpecId(specId) + if (!project) return null + return formProjectRepo.readFile(project.slug, 'main', `source/${project.slug}.pdf`) + }, + async getFieldMapping(specId, specVersion) { + const project = await findProjectBySpecId(specId) + if (!project) return null + const buf = await formProjectRepo.readFile(project.slug, specVersion, 'forms/default/field-mapping.json') + if (!buf) return null + return JSON.parse(buf.toString()) + }, + }), +) +app.route('/', projectFormRouter) +``` + +The `getSpecs` function in project-scoped mode resolves differently. Add a new dep `getSpecsByProject` alongside the existing `getSpecs`: + +```typescript +export interface FormRouterDeps { + // ... existing fields ... + /** Resolves specs for a project by owner/slug. Used in project-scoped mode. */ + getSpecsByProject?: (owner: string, slug: string, ref?: string) => Promise +} +``` + +In server.tsx, wire it to `readProjectSpecs`: + +```typescript +async getSpecsByProject(owner, slug, ref) { + const project = projectStore.getBySlug(slug) + if (!project || project.createdBy !== owner) return null + return readProjectSpecs(project.slug, ref ?? 'main') +}, +``` + +Then `resolveFormContext` uses `getSpecsByProject` when `resolveOwnerSlug` is present, and falls back to `getSpecs(specId)` in legacy mode. + +- [ ] **Step 3: Update the top-level `/forms` route to be a directory** + +The existing `forms.get('/')` handler (line 140) currently lists forms with links to `/forms/:specId`. Change it to: +- Show form title, owner, project name +- Link to `/:owner/:slug/forms` instead of `/forms/:specId` + +This requires `listSpecs()` to also return owner/slug info. Extend the return type or use the `specIdIndex` cache: + +```typescript +forms.get('/', async (c) => { + const allSpecs = await listSpecs() + return c.html( + +
+
+

Available Forms

+ My sessions +
+ {allSpecs.length === 0 ? ( +

No forms available.

+ ) : ( + + + + + + + + + + {allSpecs.map(({ dataSpec, formSpec }) => { + const entry = specIdIndex.get(dataSpec.id) + const formHref = entry + ? resolveUrl(`/${entry.owner}/${entry.slug}/forms`) + : '#' + return ( + + + + + + ) + })} + +
FormProjectDescription
+ {formSpec.title} + + {entry ? ( + + {entry.owner}/{entry.slug} + + ) : '—'} + {formSpec.description ?? ''}
+ )} +
+
, + ) +}) +``` + +Note: the `specIdIndex` is populated as a side effect of `listSpecs()`, so by the time we render, entries are available. To avoid coupling to this cache, consider having `listSpecs` return `{ dataSpec, formSpec, sha, owner, slug }` — this is cleaner. Adjust the `ResolvedSpecs` type or create a new `DirectoryEntry` type. + +- [ ] **Step 4: Update "My Sessions" to use project-scoped URLs** + +The sessions listing (line 182) currently links to `/forms/:specId/sessions/:sid/pages/0`. Update to use `/:owner/:slug/forms/sessions/:sid/pages/0`: + +```typescript +// Resolve owner/slug for each session's specId +const projectEntries = await Promise.all( + uniqueSpecIds.map(async (specId) => { + const project = await findProjectBySpecId(specId) + return [specId, project] as const + }), +) +const projectMap = new Map(projectEntries) + +// In the render: +const project = projectMap.get(s.specId) +const sessionHref = project + ? resolveUrl(`/${project.owner}/${project.slug}/forms/sessions/${s.id}/pages/0`) + : resolveUrl(`/forms/sessions/${s.id}/submission`) +``` + +Add a `resolveProjectForSpec` dep to `FormRouterDeps`: + +```typescript +resolveProjectForSpec?: (specId: string) => Promise<{ owner: string; slug: string } | null> +``` + +In server.tsx, wire it to `findProjectBySpecId`. The "My Sessions" handler uses this to resolve project context for each session's spec. + +- [ ] **Step 5: Run tests** + +Run: `bun test test/forms/` +Expected: PASS for both project-scoped and directory tests. + +- [ ] **Step 6: Commit** + +```bash +git add src/entrypoints/app/server.tsx src/entrypoints/app/routes/forms/index.tsx test/forms/ +git commit -m "feat(forms): mount project-scoped form routes, update /forms directory" +``` + +--- + +### Task 6: Create `/projects` Directory Route + +Add a new route that lists all projects on the instance. + +**Files:** +- Create: `src/entrypoints/app/routes/projects.tsx` +- Modify: `src/entrypoints/app/server.tsx` (mount the route) +- Test: `test/projects/directory-route.test.ts` + +- [ ] **Step 1: Write the failing test** + +```typescript +import { describe, expect, it } from 'bun:test' + +describe('GET /projects', () => { + it('lists all projects with links to project pages', async () => { + const res = await app.request('/projects') + expect(res.status).toBe(200) + const html = await res.text() + expect(html).toContain('Projects') + // Should contain project links + expect(html).toContain('/alice/my-project') + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Run: `bun test test/projects/directory-route.test.ts` +Expected: FAIL — 404. + +- [ ] **Step 3: Create the projects directory route** + +Create `src/entrypoints/app/routes/projects.tsx`: + +```tsx +import type { Context } from 'hono' +import type { ProjectService } from '../../../services/projects' +import { resolveUrl } from '../../../shared/base-path' +import { Layout } from '../../../design-system/components/flex-layout' +import { Breadcrumb } from '../../../design-system/components/flex-breadcrumb' + +export function projectsDirectoryHandler(projectService: ProjectService) { + return (c: Context) => { + const user = c.get('user') + const projects = projectService.listAllProjects() + .filter(p => p.status === 'ready') + return c.html( + + +
+

Projects

+ {projects.length === 0 ? ( +

No projects yet.

+ ) : ( + + + + + + + + + {projects.map(project => ( + + + + + ))} + +
ProjectOwner
+ + {project.name} + + + + {project.createdBy} + +
+ )} +
+
, + ) + } +} +``` + +- [ ] **Step 4: Mount the route in server.tsx** + +Before the `/forms` mount, add: + +```typescript +import { projectsDirectoryHandler } from './routes/projects' + +app.get('/projects', projectsDirectoryHandler(projectService)) +``` + +- [ ] **Step 5: Run test to verify it passes** + +Run: `bun test test/projects/directory-route.test.ts` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/entrypoints/app/routes/projects.tsx src/entrypoints/app/server.tsx test/projects/directory-route.test.ts +git commit -m "feat(projects): add /projects directory page" +``` + +--- + +### Task 7: Update Header Nav "Projects" Link + +Change the "Projects" header link from `/:user` to `/projects`. + +**Files:** +- Modify: `src/design-system/components/flex-layout/index.tsx:82-123` + +- [ ] **Step 1: Write the failing test** + +```typescript +describe('Layout header nav', () => { + it('links Projects to /projects instead of user profile', () => { + const html = ( + +

content

+
+ ).toString() + expect(html).toContain('href="/projects"') + expect(html).not.toContain('href="/alice"') + }) +}) +``` + +- [ ] **Step 2: Run test to verify it fails** + +Expected: FAIL — href still points to `/alice`. + +- [ ] **Step 3: Update the Projects nav item** + +In `src/design-system/components/flex-layout/index.tsx`, change: + +```tsx + +``` + +This replaces the current `href={resolveUrl(\`/\${props.user.login}\`)}`. + +- [ ] **Step 4: Run test to verify it passes** + +Run: `bun test test/design-system/flex-layout.test.ts` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/design-system/components/flex-layout/index.tsx test/design-system/ +git commit -m "feat(nav): link Projects header nav to /projects directory" +``` + +--- + +### Task 8: Unify Breadcrumbs Across All Pages + +Replace all custom breadcrumb HTML with the `flex-breadcrumb` component. Add breadcrumbs to pages that don't have them. + +**Files:** +- Modify: `src/entrypoints/app/routes/owner/components.tsx` — replace custom breadcrumb HTML in ProjectOverview, PullRequestsPage, tree/blob views, commits page +- Modify: `src/entrypoints/app/routes/forms/index.tsx` — add breadcrumbs to form pages +- Modify: `src/entrypoints/app/routes/catalog/*.tsx` — verify catalog already uses the component (it does) +- Modify: `src/entrypoints/app/server.tsx` — add breadcrumbs to dashboard, settings + +The `flex-breadcrumb` component API: +```typescript +interface BreadcrumbItem { label: string; href?: string } + +``` +Last item should omit `href` (rendered without a link, marked `aria-current="page"`). + +- [ ] **Step 1: Write a test for breadcrumbs on form pages** + +```typescript +describe('form page breadcrumbs', () => { + it('renders project-scoped breadcrumbs on form landing', async () => { + const res = await app.request('/alice/my-project/forms') + const html = await res.text() + expect(html).toContain('flex-breadcrumb') + expect(html).toContain('alice') + expect(html).toContain('my-project') + }) +}) +``` + +- [ ] **Step 2: Add breadcrumbs to form router handlers** + +In each handler in `src/entrypoints/app/routes/forms/index.tsx`, when rendering in project-scoped mode, add a `Breadcrumb` component: + +```tsx +import { Breadcrumb } from '../../../../design-system/components/flex-breadcrumb' + +// In handleLanding: + + +// In handleRenderPage: + + +// In handleReview: + +``` + +- [ ] **Step 3: Replace custom breadcrumbs in owner routes** + +In `src/entrypoints/app/routes/owner/components.tsx`, the `ProjectOverview` component (line 214) has custom breadcrumb HTML: + +```tsx + +``` + +Replace all instances of this pattern with: + +```tsx + +``` + +Do this for every component that has a `repo-header__path` nav: ProjectOverview, PullRequestsPage, tree browser, blob viewer, commits page, settings page. + +For the tree/blob views, extend the breadcrumb with path segments: + +```tsx +// Tree view: daniel / my-project / tree / main / src + ({ + label: seg, + href: i < pathSegments.length - 1 + ? resolveUrl(`/${owner}/${slug}/tree/${ref}/${pathSegments.slice(0, i + 1).join('/')}`) + : undefined, + })), +]} /> +``` + +- [ ] **Step 4: Add breadcrumbs to top-level pages** + +For `/forms` directory: +```tsx + +``` + +For `/forms/sessions`: +```tsx + +``` + +For `/projects`: +```tsx + +``` + +For user profile `/:owner`: +```tsx + +``` + +- [ ] **Step 5: Run all tests** + +Run: `bun test` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add src/entrypoints/app/routes/ src/design-system/ +git commit -m "feat(nav): unify breadcrumbs using flex-breadcrumb component" +``` + +--- + +### Task 9: Migrate All `currentPath` Usages to `currentSection` + +Now that `currentSection` is supported, update every `` call to use it and remove the deprecated `currentPath` prop. + +**Files:** +- Modify: `src/entrypoints/app/server.tsx` — dashboard, landing, error pages +- Modify: `src/entrypoints/app/routes/forms/index.tsx` — all form pages +- Modify: `src/entrypoints/app/routes/owner/index.tsx` — all owner pages +- Modify: `src/entrypoints/app/routes/catalog/*.tsx` — all catalog pages +- Modify: `src/design-system/components/flex-layout/index.tsx` — remove `currentPath` prop and fallback logic + +- [ ] **Step 1: Search for all `currentPath` usages** + +Run: `grep -rn 'currentPath' src/` + +This will list every file and line. Update each one: +- `currentPath="/"` → `currentSection="home"` +- `currentPath="/forms"` → `currentSection="forms"` +- `currentPath="/catalog"` or startsWith catalog → `currentSection="catalog"` +- Owner/project pages → `currentSection="projects"` +- Settings → `currentSection="projects"` (settings is project-scoped) + +- [ ] **Step 2: Update all Layout calls** + +Go through each file and replace. Example changes: + +```tsx +// server.tsx - dashboard + + +// forms/index.tsx - all form pages + + +// owner/index.tsx - project pages + + +// catalog - all catalog pages + +``` + +- [ ] **Step 3: Remove `currentPath` from Layout props** + +Once all callers are migrated, remove `currentPath` from `LayoutProps` and the fallback logic in the nav highlighting. + +- [ ] **Step 4: Run all tests** + +Run: `bun test` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/ +git commit -m "refactor(layout): migrate all pages from currentPath to currentSection" +``` + +--- + +### Task 10: Add RepoNav to Form Pages + +Form pages rendered under `/:owner/:slug/forms/...` should include the `RepoNav` component so users can navigate between project sections. + +**Files:** +- Modify: `src/entrypoints/app/routes/forms/index.tsx` — add RepoNav to project-scoped form handlers +- Modify: `src/entrypoints/app/routes/owner/components.tsx` — export RepoNav (currently private) + +- [ ] **Step 1: Export RepoNav** + +In `src/entrypoints/app/routes/owner/components.tsx`, change `RepoNav` from `const` to an export: + +```typescript +export const RepoNav: FC<{ + owner: string + slug: string + current: RepoTab +}> = ({ owner, slug, current }) => { +``` + +Also export the `RepoTab` type: + +```typescript +export type RepoTab = 'overview' | 'forms' | 'pulls' | 'history' | 'files' +``` + +- [ ] **Step 2: Add RepoNav to form page handlers** + +In the project-scoped form handlers, after the breadcrumb, render: + +```tsx +import { RepoNav } from '../owner/components' + +// In handleLanding (project-scoped): + +``` + +Add this to: handleLanding, handleRenderPage, handleReview, handleConfirmation, handleSubmit, handleChatView. + +- [ ] **Step 3: Write test** + +```typescript +it('renders RepoNav on project-scoped form pages', async () => { + const res = await app.request('/alice/my-project/forms') + const html = await res.text() + expect(html).toContain('repo-nav') + expect(html).toContain('repo-nav__link--current') +}) +``` + +- [ ] **Step 4: Run tests** + +Run: `bun test test/forms/` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/entrypoints/app/routes/forms/index.tsx src/entrypoints/app/routes/owner/components.tsx test/forms/ +git commit -m "feat(nav): add RepoNav to project-scoped form pages" +``` + +--- + +### Task 11: Update Existing Tests + +Update all existing test files that reference old `/forms/:specId/...` URL patterns. Tests for the legacy routes should be kept working until Task 12 removes them (or updated to test the new routes). + +**Files:** +- Modify: `test/forms/routes.test.ts` +- Modify: `test/forms/conversational-integration.test.ts` +- Modify: `test/forms/pdf-download.test.ts` +- Modify: Other test files in `test/forms/` + +- [ ] **Step 1: Inventory all affected test files** + +Run: `grep -rn "'/forms/" test/` to find all references. + +- [ ] **Step 2: Update test helpers to mount project-scoped routes** + +Update `createTestApp()` in each test file to mount the form router in project-scoped mode: + +```typescript +function createTestApp() { + const app = new Hono() + app.use('*', async (c, next) => { + c.set('user', TEST_USER) + await next() + }) + app.route( + '/:owner/:slug/forms', + createFormRouter({ + sessionGateway, + submissionGateway, + getSpecs: async (specId) => specRegistry.get(specId) ?? null, + listSpecs: async () => [...specRegistry.values()], + resolveOwnerSlug: (c) => ({ + owner: c.req.param('owner')!, + slug: c.req.param('slug')!, + }), + }), + ) + return app +} +``` + +- [ ] **Step 3: Update test request URLs** + +Change all `app.request('/forms/benefits-app/...')` to `app.request('/alice/test-project/forms/...')`. + +- [ ] **Step 4: Update assertions for new URL patterns** + +Any assertions checking for URLs in the response HTML (redirects, links) need updating: +- `/forms/benefits-app/sessions/` → `/alice/test-project/forms/sessions/` +- etc. + +- [ ] **Step 5: Run all tests** + +Run: `bun test` +Expected: PASS + +- [ ] **Step 6: Commit** + +```bash +git add test/ +git commit -m "test(forms): update tests for project-scoped form routes" +``` + +--- + +### Task 12: Remove Legacy `/forms/:specId` Routes + +Once project-scoped routes are working and tested, remove the legacy specId-based route registrations. + +**Files:** +- Modify: `src/entrypoints/app/routes/forms/index.tsx` — remove `/:specId/*` route registrations and the `formPathPrefix` function +- Modify: `src/entrypoints/app/server.tsx` — simplify the `/forms` mount to only handle directory and sessions + +- [ ] **Step 1: Remove specId route registrations** + +In `createFormRouter`, remove the `else` branch that registers `/:specId` routes. Only keep the project-scoped routes (registered when `resolveOwnerSlug` is present) and the top-level `/` (directory) and `/sessions` routes. + +- [ ] **Step 2: Remove `formPathPrefix` helper** + +Delete the `formPathPrefix` function (lines 91-95) — it's no longer needed. Only `projectFormPathPrefix` remains. + +- [ ] **Step 3: Clean up `FormRouterDeps`** + +If `resolveOwnerSlug` is now always required, remove the `?` optional marker and simplify `resolveFormContext` to remove the legacy branch. + +- [ ] **Step 4: Run all tests** + +Run: `bun test` +Expected: PASS + +- [ ] **Step 5: Commit** + +```bash +git add src/entrypoints/app/routes/forms/index.tsx src/entrypoints/app/server.tsx +git commit -m "refactor(forms): remove legacy specId-based form routes" +``` + +--- + +### Task 13: Smoke Test — End-to-End Walkthrough + +Start the dev server and manually walk through the app to verify AC 4: "A brief walkthrough of the app from landing page through form creation shows no jarring transitions." + +- [ ] **Step 1: Start the dev server** + +Run: `bun run dev` + +- [ ] **Step 2: Walk through as an anonymous user** + +1. Visit `/` — see landing page, no breadcrumbs (home page) +2. Click "Forms" in header → `/forms` — see form directory with breadcrumb "Forms" +3. Click "Projects" in header → `/projects` — see project directory with breadcrumb "Projects" +4. Click "Catalog" in header → `/catalog` — see catalog with sidebar and breadcrumb + +- [ ] **Step 3: Sign in and walk through as authenticated user** + +1. Visit `/` — see dashboard with recent projects +2. Click "Projects" → `/projects` — directory lists all projects. Header highlights "Projects" +3. Click a project → `/:owner/:slug` — project overview with breadcrumb `owner / slug`. RepoNav shows Overview (current), Forms, Pull Requests, History, Files +4. Click "Forms" tab → `/:owner/:slug/forms` — form landing with breadcrumb `owner / slug / Forms`. RepoNav highlights "Forms" +5. Click "Start now" → creates session, redirects to page 1. Breadcrumb: `owner / slug / Forms / Page 1 of N` +6. Fill form, advance pages — breadcrumb updates page number +7. Reach review → breadcrumb: `owner / slug / Forms / Review` +8. Submit → confirmation page → breadcrumb: `owner / slug / Forms / Confirmation` +9. Click `owner` in breadcrumb → back to user profile +10. Click `slug` in breadcrumb → back to project overview +11. RepoNav always visible on form pages — can click "Overview" to go back to project + +- [ ] **Step 4: Check header nav highlighting** + +Verify at each step that the correct header nav item is highlighted: +- Dashboard → "Home" +- `/forms` → "Forms" +- `/:owner/:slug/forms/...` → "Forms" +- `/projects` → "Projects" +- `/:owner`, `/:owner/:slug`, `/:owner/:slug/tree/...` → "Projects" +- `/catalog/...` → "Catalog" + +- [ ] **Step 5: Run full check suite** + +Run: `bun run check` +Expected: PASS (lint + type check + tests) + +- [ ] **Step 6: Commit any fixes** + +If the walkthrough revealed issues, fix them and commit: + +```bash +git add -A +git commit -m "fix(nav): address issues found during walkthrough" +``` diff --git a/notes/story-117-ux-consistency/review.md b/notes/story-117-ux-consistency/review.md new file mode 100644 index 000000000..6121effb1 --- /dev/null +++ b/notes/story-117-ux-consistency/review.md @@ -0,0 +1,28 @@ +# Story #117 Code Review Summary + +**Date:** 2026-05-06 +**Branch:** story-117/ux-consistency +**Reviewer:** Code review subagent + +## AC Coverage + +| AC | Status | Notes | +|----|--------|-------| +| All navigable pages include consistent breadcrumb trails | Addressed | flex-breadcrumb component used everywhere, custom HTML removed | +| Navigation patterns are unified | Addressed | Repo-centric model, project-scoped forms, consistent currentSection highlighting | +| Design system components have complete contracts | Out of scope | Split to separate story per design decision | +| Walkthrough shows no jarring transitions | Addressed | Consistent chrome from landing -> project -> form -> back | + +## Issues Found and Resolved + +1. **Owner routes missing `currentSection="projects"`** — All Layout calls in owner/index.tsx, edit/index.tsx, and compare/index.tsx were missing the prop. Fixed: added `currentSection="projects"` to all ~20 call sites. + +2. **Missing RepoNav on validation error re-render** — When form validation fails and the page is re-rendered, RepoNav was not included. Fixed: added RepoNav to the validation error path. + +## Remaining Concerns (Non-blocking) + +- **Duplicated router configuration in server.tsx** — The `/forms` and `/:owner/:slug/forms` mounts share nearly identical dep wiring (~50 lines). Could be extracted into a shared factory. Low risk, maintainability improvement. + +- **Breadcrumb missing from chat view** — `handleChatView` renders RepoNav but not Breadcrumb. The full-width layout makes this less noticeable. Minor inconsistency. + +- **Tree/blob breadcrumb hrefs** — Intermediate breadcrumb items (tree, ref) lack href links. Functionally harmless but slightly degraded from the original implementation. diff --git a/src/design-system/components/flex-accordion/styles.css b/src/design-system/components/flex-accordion/styles.css index 4fb173b75..7ad92ed4b 100644 --- a/src/design-system/components/flex-accordion/styles.css +++ b/src/design-system/components/flex-accordion/styles.css @@ -26,7 +26,7 @@ flex-accordion { background-color: var(--flex-color-bg-subtle); border: none; border-block-end: 1px solid var(--flex-color-border); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); font-weight: 700; color: var(--flex-color-text); cursor: pointer; @@ -65,7 +65,7 @@ flex-accordion { .flex-accordion__content { padding: var(--flex-space-md); border-block-end: 1px solid var(--flex-color-border); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); color: var(--flex-color-text); line-height: 1.5; diff --git a/src/design-system/components/flex-alert/styles.css b/src/design-system/components/flex-alert/styles.css index e4b6e1974..eaf1f4a8c 100644 --- a/src/design-system/components/flex-alert/styles.css +++ b/src/design-system/components/flex-alert/styles.css @@ -99,13 +99,13 @@ .flex-alert__heading { font-weight: 700; margin-block-end: var(--flex-space-xs); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; } .flex-alert__text { font-weight: 400; margin: 0; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; } diff --git a/src/design-system/components/flex-assistant/styles.css b/src/design-system/components/flex-assistant/styles.css index 2a40f3387..bbdd009c3 100644 --- a/src/design-system/components/flex-assistant/styles.css +++ b/src/design-system/components/flex-assistant/styles.css @@ -56,7 +56,7 @@ flex-assistant[data-closed] { .assistant__message { padding: var(--flex-space-sm) var(--flex-space-md); border-radius: var(--flex-radius-md, 0.5rem); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); line-height: 1.5; max-width: 90%; color: var(--flex-color-text); diff --git a/src/design-system/components/flex-badge/styles.css b/src/design-system/components/flex-badge/styles.css index 07520a8c3..120042e5e 100644 --- a/src/design-system/components/flex-badge/styles.css +++ b/src/design-system/components/flex-badge/styles.css @@ -1,6 +1,6 @@ .badge { display: inline-block; - font-size: var(--flex-text-tag); + font-size: var(--flex-text-2xs); font-weight: 600; padding: 0.125rem 0.5rem; border-radius: var(--flex-radius-bubble); diff --git a/src/design-system/components/flex-banner/styles.css b/src/design-system/components/flex-banner/styles.css index bb462a39d..e9873c190 100644 --- a/src/design-system/components/flex-banner/styles.css +++ b/src/design-system/components/flex-banner/styles.css @@ -6,7 +6,7 @@ flex-banner { } .flex-banner { - font-size: 0.87rem; + font-size: var(--flex-text-2xs); line-height: 1.5; background-color: var(--flex-color-bg-subtle); padding-block-end: 0; @@ -17,7 +17,7 @@ flex-banner { .flex-banner__header { color: var(--flex-color-text); padding-block: 0.5rem; - font-size: 0.8rem; + font-size: var(--flex-text-3xs); min-block-size: 0; } @@ -39,7 +39,7 @@ flex-banner { display: flex; align-items: center; gap: 0.5rem; - font-size: 0.8rem; + font-size: var(--flex-text-3xs); line-height: 1.1; } } @@ -65,7 +65,7 @@ flex-banner { margin: 0; color: var(--flex-color-link); display: block; - font-size: 0.8rem; + font-size: var(--flex-text-3xs); block-size: auto; line-height: 1.1; padding: 0; @@ -112,7 +112,7 @@ flex-banner { padding-inline: var(--flex-space-md); padding-block: 0.25rem 1rem; background-color: transparent; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); overflow: hidden; color: var(--flex-color-text); } @@ -135,7 +135,7 @@ flex-banner { & p { margin-block: 0 0.25rem; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); line-height: 1.5; &:last-child { @@ -160,7 +160,7 @@ flex-banner { .flex-banner__guidance-text p { margin-block: 0 0.25rem; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); line-height: 1.5; &:last-child { diff --git a/src/design-system/components/flex-branch-switcher/styles.css b/src/design-system/components/flex-branch-switcher/styles.css index 0d5ee0662..f5c6c60fa 100644 --- a/src/design-system/components/flex-branch-switcher/styles.css +++ b/src/design-system/components/flex-branch-switcher/styles.css @@ -16,7 +16,7 @@ flex-branch-switcher { background: var(--flex-color-surface); color: var(--flex-color-text); font-family: var(--flex-font-mono); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); cursor: pointer; } @@ -62,7 +62,7 @@ flex-branch-switcher { border: 1px solid var(--flex-color-border); border-radius: var(--flex-radius-sm); font-family: var(--flex-font-sans); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); } .flex-branch-switcher__list { @@ -94,7 +94,7 @@ flex-branch-switcher { flex: 1; color: var(--flex-color-text); font-family: var(--flex-font-mono); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); text-decoration: none; } @@ -124,7 +124,7 @@ flex-branch-switcher { border: 1px solid var(--flex-color-border); border-radius: var(--flex-radius-sm); font-family: var(--flex-font-mono); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); } .flex-branch-switcher__create-submit { @@ -133,7 +133,7 @@ flex-branch-switcher { border-radius: var(--flex-radius-sm); background: var(--flex-color-accent); color: var(--flex-color-on-accent); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); cursor: pointer; } @@ -148,7 +148,7 @@ flex-branch-switcher { } .flex-branch-switcher__badge { - font-size: 0.75rem; + font-size: var(--flex-text-3xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; @@ -159,7 +159,7 @@ flex-branch-switcher { } .flex-branch-switcher__trigger-meta { - font-size: 0.82rem; + font-size: var(--flex-text-3xs); color: var(--flex-color-text-muted); font-weight: 400; } diff --git a/src/design-system/components/flex-breadcrumb/styles.css b/src/design-system/components/flex-breadcrumb/styles.css index adbde4c37..781a36609 100644 --- a/src/design-system/components/flex-breadcrumb/styles.css +++ b/src/design-system/components/flex-breadcrumb/styles.css @@ -2,7 +2,7 @@ Variants: default, wrap */ .flex-breadcrumb { - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); background-color: var(--flex-color-bg); diff --git a/src/design-system/components/flex-button/styles.css b/src/design-system/components/flex-button/styles.css index 4de9f361c..f5551a1c4 100644 --- a/src/design-system/components/flex-button/styles.css +++ b/src/design-system/components/flex-button/styles.css @@ -4,7 +4,7 @@ States: hover, active, focus-visible, disabled */ .flex-button { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); font-weight: 700; line-height: 0.9; color: var(--flex-color-on-accent); diff --git a/src/design-system/components/flex-card/styles.css b/src/design-system/components/flex-card/styles.css index 78cffa335..905a8c56b 100644 --- a/src/design-system/components/flex-card/styles.css +++ b/src/design-system/components/flex-card/styles.css @@ -16,7 +16,7 @@ .flex-card__container { color: var(--flex-color-text); background-color: var(--flex-color-surface); - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.5; border-width: 2px; border-color: var(--flex-gray-cool-10); @@ -280,7 +280,7 @@ } .content-card p { - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); color: var(--flex-color-text-muted); line-height: 1.5; } @@ -293,7 +293,7 @@ } .catalog-group-label { - font-size: var(--flex-font-size-xs); + font-size: var(--flex-text-xs); font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; diff --git a/src/design-system/components/flex-character-count/styles.css b/src/design-system/components/flex-character-count/styles.css index ffbc36c77..d8ab86279 100644 --- a/src/design-system/components/flex-character-count/styles.css +++ b/src/design-system/components/flex-character-count/styles.css @@ -8,7 +8,7 @@ flex-character-count { .flex-character-count__message { display: inline-block; padding-block-start: 0.25rem; - font-size: 1rem; + font-size: var(--flex-text-sm); color: var(--flex-color-text); line-height: 1.3; diff --git a/src/design-system/components/flex-collection/styles.css b/src/design-system/components/flex-collection/styles.css index dcee26e3e..d0a4981a8 100644 --- a/src/design-system/components/flex-collection/styles.css +++ b/src/design-system/components/flex-collection/styles.css @@ -2,7 +2,7 @@ Variants: default, condensed */ .flex-collection { - font-size: 1.06rem; + font-size: var(--flex-text-md); margin-block: 1em; line-height: 1.5; padding-inline-start: 0; @@ -94,7 +94,7 @@ .flex-collection__meta-item { margin-block-start: 0.25rem; - font-size: 0.93rem; + font-size: var(--flex-text-xs); line-height: 1.3; display: block; margin-inline-end: 0.5rem; diff --git a/src/design-system/components/flex-combo-box/styles.css b/src/design-system/components/flex-combo-box/styles.css index bb3db0c91..432c9c20e 100644 --- a/src/design-system/components/flex-combo-box/styles.css +++ b/src/design-system/components/flex-combo-box/styles.css @@ -16,7 +16,7 @@ flex-combo-box { /* Input — extends form control base styles */ .flex-combo-box__input { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); background-color: var(--flex-color-surface); @@ -100,7 +100,7 @@ flex-combo-box { /* Options */ .flex-combo-box__option { padding: 0.5rem; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); cursor: pointer; @@ -129,7 +129,7 @@ flex-combo-box { /* No results message */ .flex-combo-box__no-results { padding: 0.5rem; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text-muted); font-style: italic; diff --git a/src/design-system/components/flex-confidence-badge/styles.css b/src/design-system/components/flex-confidence-badge/styles.css index 2573de78c..bc6a2406a 100644 --- a/src/design-system/components/flex-confidence-badge/styles.css +++ b/src/design-system/components/flex-confidence-badge/styles.css @@ -2,7 +2,7 @@ .flex-confidence-badge { display: inline-block; - font-size: var(--flex-text-tag); + font-size: var(--flex-text-2xs); font-weight: 600; padding: 0.125rem 0.5rem; border-radius: var(--flex-radius-bubble); diff --git a/src/design-system/components/flex-date-picker/styles.css b/src/design-system/components/flex-date-picker/styles.css index a42ce1787..340cae17f 100644 --- a/src/design-system/components/flex-date-picker/styles.css +++ b/src/design-system/components/flex-date-picker/styles.css @@ -16,7 +16,7 @@ flex-date-picker { /* Input — extends form control base styles */ .flex-date-picker__external-input { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); background-color: var(--flex-color-surface); @@ -120,7 +120,7 @@ flex-date-picker { .flex-date-picker__month-label { flex: 1; padding: 0.5rem; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); font-weight: 700; text-align: center; background-color: var(--flex-gray-5); @@ -147,7 +147,7 @@ flex-date-picker { } .flex-date-picker__table th { - font-size: 0.87rem; + font-size: var(--flex-text-2xs); font-weight: 400; color: var(--flex-color-text-muted); padding: 0.375rem 0; @@ -165,7 +165,7 @@ flex-date-picker { inline-size: 100%; aspect-ratio: 1; padding: 0; - font-size: 0.93rem; + font-size: var(--flex-text-xs); line-height: 1; color: var(--flex-color-text); background-color: var(--flex-gray-5); @@ -227,7 +227,7 @@ flex-date-picker { justify-content: center; inline-size: 100%; padding: 0.75rem; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); color: var(--flex-color-text); background-color: var(--flex-gray-5); border: none; diff --git a/src/design-system/components/flex-diagram/styles.css b/src/design-system/components/flex-diagram/styles.css index 03db3d12f..c53c07edd 100644 --- a/src/design-system/components/flex-diagram/styles.css +++ b/src/design-system/components/flex-diagram/styles.css @@ -16,12 +16,12 @@ .flex-diagram__node text { fill: var(--flex-color-text); - font-size: 14px; + font-size: var(--flex-text-2xs); } .flex-diagram__node-sublabel { fill: var(--flex-color-text-muted); - font-size: 11px; + font-size: var(--flex-text-3xs); } /* Linked nodes */ @@ -51,7 +51,7 @@ .flex-diagram__edge-label { fill: var(--flex-color-text-muted); - font-size: 12px; + font-size: var(--flex-text-3xs); } /* Arrowhead */ @@ -69,6 +69,6 @@ .flex-diagram__group-label { fill: var(--flex-color-text-muted); - font-size: 12px; + font-size: var(--flex-text-3xs); font-style: italic; } diff --git a/src/design-system/components/flex-file-input/styles.css b/src/design-system/components/flex-file-input/styles.css index a7e0c572a..18d65a1fa 100644 --- a/src/design-system/components/flex-file-input/styles.css +++ b/src/design-system/components/flex-file-input/styles.css @@ -10,7 +10,7 @@ flex-file-input { .flex-file-input__target { border: 1px dashed var(--flex-gray-cool-30); display: block; - font-size: 0.93rem; + font-size: var(--flex-text-xs); margin-block-start: 0.5rem; position: relative; text-align: center; @@ -123,7 +123,7 @@ flex-file-input { } .flex-file-input__file-name { - font-size: 0.93rem; + font-size: var(--flex-text-xs); font-weight: 700; color: var(--flex-color-text); overflow: hidden; @@ -132,7 +132,7 @@ flex-file-input { } .flex-file-input__file-size { - font-size: 0.8rem; + font-size: var(--flex-text-3xs); color: var(--flex-color-text-muted); } diff --git a/src/design-system/components/flex-footer/styles.css b/src/design-system/components/flex-footer/styles.css index a43d2d2a3..d41168ed3 100644 --- a/src/design-system/components/flex-footer/styles.css +++ b/src/design-system/components/flex-footer/styles.css @@ -2,7 +2,7 @@ Variants: slim (default), medium, big */ .flex-footer { - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.5; } @@ -122,7 +122,7 @@ .flex-footer__logo-heading { margin: 0; - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.1; } diff --git a/src/design-system/components/flex-form-landing/index.tsx b/src/design-system/components/flex-form-landing/index.tsx index 732adba50..f86dae704 100644 --- a/src/design-system/components/flex-form-landing/index.tsx +++ b/src/design-system/components/flex-form-landing/index.tsx @@ -10,12 +10,17 @@ interface FormLandingSpec { interface FormLandingProps { formSpec: FormLandingSpec startUrl: string + hideTitle?: boolean } -export const FormLanding: FC = ({ formSpec, startUrl }) => { +export const FormLanding: FC = ({ + formSpec, + startUrl, + hideTitle, +}) => { return (
-

{formSpec.title}

+ {!hideTitle &&

{formSpec.title}

} {formSpec.description &&

{formSpec.description}

}

This form has {formSpec.pages.length} sections.

diff --git a/src/design-system/components/flex-form/styles.css b/src/design-system/components/flex-form/styles.css index 244265626..87cef575a 100644 --- a/src/design-system/components/flex-form/styles.css +++ b/src/design-system/components/flex-form/styles.css @@ -3,7 +3,7 @@ Large variant: data-size="large" — max-width 46rem (matches usa-form--large) */ .flex-form { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; /* stylelint-disable-next-line declaration-property-value-allowed-list -- USWDS usa-form intrinsic width. */ max-inline-size: 32em; diff --git a/src/design-system/components/flex-header/styles.css b/src/design-system/components/flex-header/styles.css index b428b20bf..00aa4cd83 100644 --- a/src/design-system/components/flex-header/styles.css +++ b/src/design-system/components/flex-header/styles.css @@ -6,7 +6,7 @@ flex-header { } .flex-header { - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.5; background-color: var(--flex-color-surface); border-block-end: 1px solid var(--flex-color-border); @@ -52,7 +52,7 @@ flex-header { border: 0; border-radius: 0; box-shadow: none; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); font-weight: 400; block-size: 3rem; padding-inline: 0.75rem; @@ -91,7 +91,7 @@ flex-header { border: 0; border-radius: 0; box-shadow: none; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); font-weight: 400; color: currentColor; text-decoration: underline; @@ -129,7 +129,7 @@ flex-header { /* --- Nav items --- */ .flex-header__nav-item { - font-size: 0.93rem; + font-size: var(--flex-text-xs); line-height: 0.9; } @@ -246,7 +246,7 @@ flex-header { } .flex-header__user-login { - font-size: 0.85rem; + font-size: var(--flex-text-2xs); color: var(--flex-color-text-muted); line-height: 1.2; } diff --git a/src/design-system/components/flex-identifier/styles.css b/src/design-system/components/flex-identifier/styles.css index cce99ae5f..b1a6decdc 100644 --- a/src/design-system/components/flex-identifier/styles.css +++ b/src/design-system/components/flex-identifier/styles.css @@ -2,7 +2,7 @@ Agency identification with logos, required links, and disclaimers */ .flex-identifier { - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-white); background-color: var(--flex-gray-90); diff --git a/src/design-system/components/flex-in-page-nav/styles.css b/src/design-system/components/flex-in-page-nav/styles.css index 6c3749797..a4a172c6a 100644 --- a/src/design-system/components/flex-in-page-nav/styles.css +++ b/src/design-system/components/flex-in-page-nav/styles.css @@ -15,7 +15,7 @@ flex-in-page-nav { } .flex-in-page-nav__heading { - font-size: 0.87rem; + font-size: var(--flex-text-2xs); font-weight: 700; line-height: 1.3; color: var(--flex-color-text); @@ -42,7 +42,7 @@ flex-in-page-nav { .flex-in-page-nav__link { display: block; padding: 0.5rem 1rem; - font-size: 1rem; + font-size: var(--flex-text-sm); line-height: 1.15; color: var(--flex-color-accent); text-decoration: none; diff --git a/src/design-system/components/flex-input-mask/styles.css b/src/design-system/components/flex-input-mask/styles.css index 1477ee53f..500fdf9e7 100644 --- a/src/design-system/components/flex-input-mask/styles.css +++ b/src/design-system/components/flex-input-mask/styles.css @@ -27,7 +27,7 @@ flex-input-mask { /* Match the input's padding so mask characters align with typed text */ padding: var(--flex-control-padding); margin-block-start: var(--flex-control-margin-top); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; block-size: var(--flex-control-height); display: flex; diff --git a/src/design-system/components/flex-input-prefix-suffix/styles.css b/src/design-system/components/flex-input-prefix-suffix/styles.css index d54d2e41e..f329ee88e 100644 --- a/src/design-system/components/flex-input-prefix-suffix/styles.css +++ b/src/design-system/components/flex-input-prefix-suffix/styles.css @@ -3,7 +3,7 @@ States: error, success (from parent group) */ .flex-input-group { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; appearance: none; border: 1px solid var(--flex-gray-cool-60); diff --git a/src/design-system/components/flex-language-selector/styles.css b/src/design-system/components/flex-language-selector/styles.css index d0374469f..103f10724 100644 --- a/src/design-system/components/flex-language-selector/styles.css +++ b/src/design-system/components/flex-language-selector/styles.css @@ -13,7 +13,7 @@ flex-language-selector { & .flex-language-selector__link { padding: 0.25rem 0.5rem; font-weight: 700; - font-size: 0.87rem; + font-size: var(--flex-text-2xs); } } } @@ -27,7 +27,7 @@ flex-language-selector { background-color: transparent; border: 0; color: var(--flex-blue-vivid-60); - font-size: 0.87rem; + font-size: var(--flex-text-2xs); font-weight: 700; cursor: pointer; padding: 0.5rem 0.75rem; @@ -91,7 +91,7 @@ flex-language-selector { padding: 0.5rem 1rem; color: var(--flex-blue-vivid-60); text-decoration: none; - font-size: 0.93rem; + font-size: var(--flex-text-xs); white-space: nowrap; &:hover { diff --git a/src/design-system/components/flex-layout/index.tsx b/src/design-system/components/flex-layout/index.tsx index c9f1a3312..0bbb34835 100644 --- a/src/design-system/components/flex-layout/index.tsx +++ b/src/design-system/components/flex-layout/index.tsx @@ -12,7 +12,7 @@ import { Header, HeaderNavItem, type HeaderUser } from '../flex-header' interface LayoutProps { title?: string sidebar?: Child - currentPath?: string + currentSection?: 'home' | 'forms' | 'projects' | 'catalog' | 'admin' user?: HeaderUser | null contentWidth?: 'centered' | 'full' } @@ -28,6 +28,12 @@ function isAdminUser(login: string): boolean { export const Layout: FC> = (props) => { const title = props.title ? `${props.title} | Forms Lab` : 'Forms Lab' + const isHome = props.currentSection === 'home' + const isForms = props.currentSection === 'forms' + const isProjects = props.currentSection === 'projects' + const isCatalog = props.currentSection === 'catalog' + const isAdmin = props.currentSection === 'admin' + return ( @@ -87,33 +93,29 @@ export const Layout: FC> = (props) => { props.user ? resolveUrl('/settings/variants') : undefined } > - + {props.user ? ( <> {isAdminUser(props.user.login) && ( )} @@ -122,12 +124,17 @@ export const Layout: FC> = (props) => { + li { margin-block-end: 0; @@ -75,7 +75,7 @@ .flex-sidenav__item { border-block-start: 1px solid var(--flex-color-border); - font-size: 0.93rem; + font-size: var(--flex-text-xs); } .flex-sidenav__link { diff --git a/src/design-system/components/flex-site-alert/styles.css b/src/design-system/components/flex-site-alert/styles.css index afea68f18..8b8d523e5 100644 --- a/src/design-system/components/flex-site-alert/styles.css +++ b/src/design-system/components/flex-site-alert/styles.css @@ -4,7 +4,7 @@ Modifiers: slim, no-icon, no-heading */ .flex-site-alert { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; color: var(--flex-color-text); @@ -95,6 +95,6 @@ .flex-site-alert__text { font-weight: 400; margin: 0; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; } diff --git a/src/design-system/components/flex-spec-browser/styles.css b/src/design-system/components/flex-spec-browser/styles.css index f30e0150f..128dcfd82 100644 --- a/src/design-system/components/flex-spec-browser/styles.css +++ b/src/design-system/components/flex-spec-browser/styles.css @@ -13,7 +13,7 @@ flex-spec-browser { grid-template-columns: minmax(12rem, 16rem) minmax(0, 1fr); gap: var(--flex-space-xl); align-items: start; - font-size: 1rem; + font-size: var(--flex-text-sm); line-height: 1.4; } @@ -30,7 +30,7 @@ flex-spec-browser { } .flex-spec-browser__nav-heading { - font-size: 0.95rem; + font-size: var(--flex-text-xs); font-weight: 700; line-height: 1.3; color: var(--flex-color-text); @@ -45,7 +45,7 @@ flex-spec-browser { } .flex-spec-browser__nav-subheading { - font-size: 0.82rem; + font-size: var(--flex-text-3xs); font-weight: 600; color: var(--flex-color-text-muted); margin: 0; @@ -67,7 +67,7 @@ flex-spec-browser { .flex-spec-browser__nav-link { display: block; padding: 0.5rem 1rem; - font-size: 1rem; + font-size: var(--flex-text-sm); line-height: 1.3; color: var(--flex-color-accent); text-decoration: none; @@ -104,7 +104,7 @@ flex-spec-browser { .flex-spec-browser__nav-link--sub { padding-inline-start: 2rem; - font-size: 0.9rem; + font-size: var(--flex-text-2xs); color: var(--flex-color-text-muted); } @@ -159,7 +159,7 @@ flex-spec-browser { gap: var(--flex-space-sm); flex-wrap: wrap; font-weight: 600; - font-size: 1.06rem; + font-size: var(--flex-text-md); &::-webkit-details-marker { display: none; @@ -169,7 +169,7 @@ flex-spec-browser { content: "\25B8"; /* right-pointing triangle */ display: inline-block; color: var(--flex-color-text-muted); - font-size: 0.85em; + font-size: var(--flex-text-2xs); transition: transform 0.15s ease; flex-shrink: 0; } @@ -203,7 +203,7 @@ flex-spec-browser { gap: var(--flex-space-sm); font-weight: 400; color: var(--flex-color-text-muted); - font-size: 0.95rem; + font-size: var(--flex-text-xs); } .flex-spec-browser__panel-count { @@ -212,7 +212,7 @@ flex-spec-browser { .flex-spec-browser__delivery { display: inline-block; - font-size: var(--flex-text-tag); + font-size: var(--flex-text-2xs); font-weight: 600; padding: 0.125rem 0.5rem; border-radius: var(--flex-radius-bubble); @@ -296,7 +296,7 @@ flex-spec-browser { .flex-spec-browser__condition { font-family: var(--flex-font-mono, monospace); - font-size: 0.9rem; + font-size: var(--flex-text-2xs); background: var(--flex-color-surface); border: 1px solid var(--flex-color-border); padding: 0.05rem 0.4rem; diff --git a/src/design-system/components/flex-spec-diff-browser/styles.css b/src/design-system/components/flex-spec-diff-browser/styles.css index c96d1db91..36249bc82 100644 --- a/src/design-system/components/flex-spec-diff-browser/styles.css +++ b/src/design-system/components/flex-spec-diff-browser/styles.css @@ -14,7 +14,7 @@ flex-spec-diff-browser { flex-direction: column; gap: var(--flex-space-md); min-inline-size: 0; - font-size: 1rem; + font-size: var(--flex-text-sm); line-height: 1.4; } @@ -39,7 +39,7 @@ flex-spec-diff-browser { margin: 0; font-weight: 600; color: var(--flex-color-text); - font-size: 1rem; + font-size: var(--flex-text-sm); } .flex-spec-diff-browser__overview-jumps { @@ -94,7 +94,7 @@ flex-spec-diff-browser { gap: var(--flex-space-sm); flex-wrap: wrap; font-weight: 600; - font-size: 1.06rem; + font-size: var(--flex-text-md); &::-webkit-details-marker { display: none; @@ -104,7 +104,7 @@ flex-spec-diff-browser { content: "\25B8"; /* right-pointing triangle */ display: inline-block; color: var(--flex-color-text-muted); - font-size: 0.85em; + font-size: var(--flex-text-2xs); transition: transform 0.15s ease; flex-shrink: 0; } @@ -206,7 +206,7 @@ flex-spec-diff-browser { gap: var(--flex-space-sm); flex-wrap: wrap; font-weight: 600; - font-size: 1rem; + font-size: var(--flex-text-sm); &::-webkit-details-marker { display: none; @@ -216,7 +216,7 @@ flex-spec-diff-browser { content: "\25B8"; display: inline-block; color: var(--flex-color-text-muted); - font-size: 0.8em; + font-size: var(--flex-text-2xs); transition: transform 0.15s ease; flex-shrink: 0; } @@ -267,12 +267,12 @@ flex-spec-diff-browser { flex-wrap: wrap; align-items: baseline; gap: var(--flex-space-xs); - font-size: 1rem; + font-size: var(--flex-text-sm); } .flex-spec-diff-browser__field-row--was { color: var(--flex-color-text-muted); - font-size: 0.9rem; + font-size: var(--flex-text-2xs); text-decoration: line-through; } @@ -288,12 +288,12 @@ flex-spec-diff-browser { .flex-spec-diff-browser__field-type { color: var(--flex-color-text-muted); font-family: var(--flex-font-mono); - font-size: 0.85rem; + font-size: var(--flex-text-2xs); } .flex-spec-diff-browser__field-required { color: var(--flex-color-text-muted); - font-size: 0.75rem; + font-size: var(--flex-text-3xs); text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; @@ -303,7 +303,7 @@ flex-spec-diff-browser { .flex-spec-diff-browser__badge { display: inline-block; - font-size: var(--flex-text-tag); + font-size: var(--flex-text-2xs); font-weight: 600; padding: 0.125rem 0.5rem; border-radius: var(--flex-radius-bubble); diff --git a/src/design-system/components/flex-step-indicator/styles.css b/src/design-system/components/flex-step-indicator/styles.css index 10a0153ef..b9fcdddf7 100644 --- a/src/design-system/components/flex-step-indicator/styles.css +++ b/src/design-system/components/flex-step-indicator/styles.css @@ -2,7 +2,7 @@ Variants: no-labels, counters, small-counters, centered */ .flex-step-indicator { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.1; background-color: var(--flex-color-bg); margin-block-end: 2rem; @@ -65,7 +65,7 @@ .flex-step-indicator__segment-label { color: var(--flex-gray-cool-60); display: block; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); margin-block-start: calc(0.5rem + 0.5rem); padding-inline-end: 2rem; text-align: start; @@ -200,7 +200,7 @@ .flex-step-indicator__segment::before { block-size: 1.5rem; inline-size: 1.5rem; - font-size: 0.93rem; + font-size: var(--flex-text-xs); padding: calc(0.25rem + 1px); inset-block-start: calc((1.5rem - 0.5rem) / -2); } diff --git a/src/design-system/components/flex-strategy-selector/styles.css b/src/design-system/components/flex-strategy-selector/styles.css index fb0d90408..a71f6c551 100644 --- a/src/design-system/components/flex-strategy-selector/styles.css +++ b/src/design-system/components/flex-strategy-selector/styles.css @@ -77,7 +77,7 @@ .flex-strategy-selector__name { font-family: var(--flex-font-sans); - font-size: var(--flex-text-base); + font-size: var(--flex-text-2xs); font-weight: 600; line-height: var(--flex-line-height-heading); color: var(--flex-color-text); diff --git a/src/design-system/components/flex-summary-box/styles.css b/src/design-system/components/flex-summary-box/styles.css index 7ec927093..eb5026c10 100644 --- a/src/design-system/components/flex-summary-box/styles.css +++ b/src/design-system/components/flex-summary-box/styles.css @@ -2,7 +2,7 @@ Bordered callout with info-lighter background */ .flex-summary-box { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; color: var(--flex-color-text); background-color: var(--flex-color-info-lighter); diff --git a/src/design-system/components/flex-tab-group/styles.css b/src/design-system/components/flex-tab-group/styles.css index 767ef29d1..dad20b09a 100644 --- a/src/design-system/components/flex-tab-group/styles.css +++ b/src/design-system/components/flex-tab-group/styles.css @@ -13,7 +13,7 @@ flex-tab-group { background: none; border: 1px solid transparent; border-block-end: none; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); font-weight: 700; color: var(--flex-color-text-muted); cursor: pointer; diff --git a/src/design-system/components/flex-table/styles.css b/src/design-system/components/flex-table/styles.css index 628d3209b..9e5dff364 100644 --- a/src/design-system/components/flex-table/styles.css +++ b/src/design-system/components/flex-table/styles.css @@ -5,7 +5,7 @@ /* --- Base table --- */ .flex-table { - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.5; border-collapse: collapse; border-spacing: 0; @@ -17,7 +17,7 @@ .flex-table caption { text-align: start; - font-size: 1rem; + font-size: var(--flex-text-sm); font-weight: 700; margin-block-end: 0.75rem; } @@ -177,7 +177,7 @@ .flex-table[data-stacked-header] tr td:first-child, .flex-table[data-stacked-header] tr th:first-child { border-block-start-width: 0; - font-size: 1.06rem; + font-size: var(--flex-text-md); line-height: 1.1; background-color: var(--flex-color-table-header); color: var(--flex-color-text); diff --git a/src/design-system/components/flex-tag/styles.css b/src/design-system/components/flex-tag/styles.css index e0408b488..3172d30b7 100644 --- a/src/design-system/components/flex-tag/styles.css +++ b/src/design-system/components/flex-tag/styles.css @@ -2,7 +2,7 @@ Sizes: default, big */ .flex-tag { - font-size: 0.93rem; + font-size: var(--flex-text-xs); color: var(--flex-white); text-transform: uppercase; background-color: var(--flex-gray-cool-60); @@ -19,6 +19,6 @@ &[data-size="big"] { padding-inline: 0.5rem; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); } } diff --git a/src/design-system/components/flex-tooltip/styles.css b/src/design-system/components/flex-tooltip/styles.css index e27cde656..ece4c63e7 100644 --- a/src/design-system/components/flex-tooltip/styles.css +++ b/src/design-system/components/flex-tooltip/styles.css @@ -15,7 +15,7 @@ flex-tooltip { display: none; background-color: var(--flex-color-tooltip-bg); color: var(--flex-color-tooltip-text); - font-size: 1rem; + font-size: var(--flex-text-sm); padding: 0.5rem; border-radius: 0.25rem; white-space: pre; diff --git a/src/design-system/components/flex-validation/styles.css b/src/design-system/components/flex-validation/styles.css index b36121a4a..dc8061ba3 100644 --- a/src/design-system/components/flex-validation/styles.css +++ b/src/design-system/components/flex-validation/styles.css @@ -6,7 +6,7 @@ /* --- Inline validation message --- */ .flex-validation { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; margin-block-start: var(--flex-space-xs); @@ -24,7 +24,7 @@ /* --- Validation summary --- */ .flex-validation-summary { - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; border-inline-start: 0.25rem solid var(--flex-color-error); padding: 1rem 1.25rem; @@ -63,7 +63,7 @@ margin: 0; padding: 0; list-style: none; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.5; } diff --git a/src/design-system/components/flex-variant-callout/styles.css b/src/design-system/components/flex-variant-callout/styles.css index dfd9072b7..ceea0f97c 100644 --- a/src/design-system/components/flex-variant-callout/styles.css +++ b/src/design-system/components/flex-variant-callout/styles.css @@ -32,7 +32,7 @@ .flex-variant-callout__variant-name { display: block; - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); font-weight: 600; color: var(--flex-color-text); line-height: 1.3; diff --git a/src/entrypoints/app/public/base-classes.css b/src/entrypoints/app/public/base-classes.css index f123a118a..fca2c115e 100644 --- a/src/entrypoints/app/public/base-classes.css +++ b/src/entrypoints/app/public/base-classes.css @@ -6,7 +6,7 @@ .flex-select, .flex-textarea { font-family: var(--flex-font-sans); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); background-color: var(--flex-color-surface); @@ -58,7 +58,7 @@ .flex-error-message, .flex-legend { font-family: var(--flex-font-sans); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); } @@ -68,7 +68,7 @@ .flex-checkbox__label, .flex-radio__label { font-family: var(--flex-font-sans); - font-size: var(--flex-text-uswds); + font-size: var(--flex-text-md); line-height: 1.3; color: var(--flex-color-text); cursor: pointer; diff --git a/src/entrypoints/app/public/tokens.css b/src/entrypoints/app/public/tokens.css index 37cbc3ffc..76ca009a0 100644 --- a/src/entrypoints/app/public/tokens.css +++ b/src/entrypoints/app/public/tokens.css @@ -1662,11 +1662,18 @@ "Roboto Mono Web", "Bitstream Vera Sans Mono", "Consolas", "Courier", monospace; /* @uswds font-mono */ - --flex-text-xs: 0.7em; - --flex-text-sm: 0.82em; - --flex-text-base: 0.9em; - --flex-text-uswds: 1.06rem; /* @uswds normalized base font-size (cap-height adjustment for Source Sans Pro) */ - --flex-text-tag: 0.875rem; + /* Typography scale — USWDS 3.13 theme tokens, all rem for accessibility. + Values use USWDS's Source Sans Pro cap-height normalization, not round + numbers, so computed sizes match USWDS reference components exactly. */ + --flex-text-3xs: 0.81rem; /* 12.96px — USWDS 3xs */ + --flex-text-2xs: 0.87rem; /* 13.92px — USWDS 2xs */ + --flex-text-xs: 0.93rem; /* 14.88px — USWDS xs */ + --flex-text-sm: 1rem; /* 16px — USWDS sm */ + --flex-text-md: 1.06rem; /* 16.96px — USWDS md (normalized for Source Sans Pro) */ + --flex-text-lg: 1.37rem; /* 21.92px — USWDS lg */ + --flex-text-xl: 2rem; /* 32px — USWDS xl */ + --flex-text-2xl: 2.5rem; /* 40px — USWDS 2xl */ + --flex-text-3xl: 3rem; /* 48px — USWDS 3xl */ /* Focus tokens */ --flex-focus-ring: 4px solid var(--flex-color-accent); diff --git a/src/entrypoints/app/public/utilities.css b/src/entrypoints/app/public/utilities.css index 5ca1eeb2e..70065b2e6 100644 --- a/src/entrypoints/app/public/utilities.css +++ b/src/entrypoints/app/public/utilities.css @@ -74,6 +74,6 @@ a[href]::after { content: " (" attr(href) ")"; - font-size: 0.85em; + font-size: var(--flex-text-2xs); } } diff --git a/src/entrypoints/app/routes/admin/index.tsx b/src/entrypoints/app/routes/admin/index.tsx index 6a475398c..3ec5daf5f 100644 --- a/src/entrypoints/app/routes/admin/index.tsx +++ b/src/entrypoints/app/routes/admin/index.tsx @@ -35,7 +35,7 @@ export function createAdminRoutes( const revokedUsers = enriched(revoked) return c.html( - + { const user = c.get('user') return c.html( - + , ) @@ -269,7 +269,7 @@ export function createAuthRoutes( // GET /auth/access-pending auth.get('/access-pending', (c) => { return c.html( - + , ) @@ -278,7 +278,7 @@ export function createAuthRoutes( // GET /auth/access-denied auth.get('/access-denied', (c) => { return c.html( - + , ) diff --git a/src/entrypoints/app/routes/catalog/architecture.tsx b/src/entrypoints/app/routes/catalog/architecture.tsx index 8c600f598..35c7fb56a 100644 --- a/src/entrypoints/app/routes/catalog/architecture.tsx +++ b/src/entrypoints/app/routes/catalog/architecture.tsx @@ -62,7 +62,7 @@ architecture.get('/', async (c) => {

Architecture

@@ -112,7 +112,7 @@ architecture.get('/:slug', async (c) => { { {

Architectural Decisions

@@ -136,7 +136,7 @@ decisions.get('/:group/:slug', async (c) => { { { ] return c.html( - +

Design System

A USWDS-aligned design system built with CUBE CSS methodology. Semantic @@ -212,7 +212,7 @@ designSystem.get('/:slug', async (c) => {

Typography

@@ -409,7 +409,7 @@ designSystem.get('/:slug', async (c) => {

Tokens

@@ -485,7 +485,7 @@ designSystem.get('/:slug', async (c) => {

Compositions

@@ -534,7 +534,7 @@ designSystem.get('/:slug', async (c) => {

Rules

@@ -561,7 +561,7 @@ designSystem.get('/:slug', async (c) => {

Base Classes

@@ -798,7 +798,7 @@ designSystem.get('/:slug', async (c) => {

Data Visualizations

@@ -907,7 +907,7 @@ designSystem.get('/:slug', async (c) => {

Layout

@@ -1255,7 +1255,7 @@ designSystem.get('/:slug', async (c) => {