wize-dev-kit 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,211 @@
1
+ ---
2
+ playbook: playwright-vitest
3
+ owner: wize-agent-test-architect # Hawkeye
4
+ applies_when: web-overlay active
5
+ status: ready
6
+ ---
7
+
8
+ # Web Test Stack — Hawkeye Playbook
9
+
10
+ Two tools, one rule per: **Vitest** for fast, isolated, deterministic. **Playwright** for end-to-end through real browsers.
11
+
12
+ ## 1. What goes where
13
+
14
+ | Test type | Tool | Speed | What it proves |
15
+ |---|---|---|---|
16
+ | Pure function | Vitest | µs | A unit of logic does its job. |
17
+ | React/Vue component | Vitest + Testing Library | ms | A component renders and reacts correctly. |
18
+ | Hook / store / state | Vitest | ms | Side-effect-free transitions are correct. |
19
+ | Service / API call | Vitest + MSW | ms | Client glue handles 2xx/4xx/5xx. |
20
+ | Integration (multi-component) | Vitest + Testing Library | tens of ms | Real interactions on a fake DOM. |
21
+ | **User journey (E2E)** | Playwright | seconds | A real browser can complete the flow. |
22
+ | Visual regression | Playwright (toHaveScreenshot) | seconds | UI didn't shift unintentionally. |
23
+ | Cross-browser | Playwright (chromium/firefox/webkit) | minutes | Behavior is consistent. |
24
+
25
+ **Default split (Hawkeye `tea-design.md`):** 70/20/10 — unit/integration/E2E.
26
+
27
+ ## 2. Vitest setup (per project)
28
+
29
+ ```jsonc
30
+ // package.json
31
+ {
32
+ "scripts": {
33
+ "test": "vitest",
34
+ "test:run": "vitest run",
35
+ "test:coverage": "vitest run --coverage"
36
+ },
37
+ "devDependencies": {
38
+ "vitest": "^2",
39
+ "@vitest/coverage-v8": "^2",
40
+ "@testing-library/dom": "^10",
41
+ "@testing-library/user-event": "^14",
42
+ "happy-dom": "^15",
43
+ "msw": "^2"
44
+ }
45
+ }
46
+ ```
47
+
48
+ ```ts
49
+ // vitest.config.ts
50
+ import { defineConfig } from 'vitest/config';
51
+ export default defineConfig({
52
+ test: {
53
+ environment: 'happy-dom', // jsdom if you need broader DOM compat
54
+ globals: true,
55
+ setupFiles: ['./test/setup.ts'],
56
+ coverage: { reporter: ['text', 'lcov'], thresholds: { lines: 80, branches: 75 } }
57
+ }
58
+ });
59
+ ```
60
+
61
+ ### Test naming
62
+
63
+ `{feature}.spec.ts` for code. Stories sit beside them: `{Story}.story.tsx`. Avoid `__tests__` folders — co-locate.
64
+
65
+ ### Patterns
66
+
67
+ - Use **Testing Library** queries by role first (`getByRole('button', { name: /save/i })`). Fall back to `getByLabelText`, then `getByText`, then `getByTestId` only as a last resort.
68
+ - Use **user-event v14**, never `fireEvent`, for interactions. It mimics real user input.
69
+ - For network: **MSW** at the network boundary. Don't mock `fetch` per-test.
70
+ - Snapshot tests are fine for small, stable shapes (a discriminated union, a normalized payload). Not for component trees.
71
+
72
+ ## 3. Playwright setup
73
+
74
+ ```jsonc
75
+ // package.json
76
+ {
77
+ "scripts": {
78
+ "e2e": "playwright test",
79
+ "e2e:ui": "playwright test --ui",
80
+ "e2e:headed": "playwright test --headed"
81
+ },
82
+ "devDependencies": { "@playwright/test": "^1.50" }
83
+ }
84
+ ```
85
+
86
+ ```ts
87
+ // playwright.config.ts
88
+ import { defineConfig, devices } from '@playwright/test';
89
+ export default defineConfig({
90
+ testDir: './e2e',
91
+ fullyParallel: true,
92
+ retries: process.env.CI ? 2 : 0,
93
+ reporter: process.env.CI ? [['github'], ['html']] : 'list',
94
+ use: {
95
+ baseURL: process.env.E2E_BASE_URL ?? 'http://localhost:3000',
96
+ trace: 'on-first-retry',
97
+ screenshot: 'only-on-failure',
98
+ video: 'retain-on-failure'
99
+ },
100
+ projects: [
101
+ { name: 'chromium', use: devices['Desktop Chrome'] },
102
+ { name: 'firefox', use: devices['Desktop Firefox'] },
103
+ { name: 'webkit', use: devices['Desktop Safari'] },
104
+ { name: 'mobile-ios', use: devices['iPhone 14'] },
105
+ { name: 'mobile-android', use: devices['Pixel 7'] }
106
+ ],
107
+ webServer: process.env.CI ? undefined : {
108
+ command: 'npm run dev', port: 3000, reuseExistingServer: true
109
+ }
110
+ });
111
+ ```
112
+
113
+ ### Page Object Model (light)
114
+
115
+ ```ts
116
+ // e2e/pages/signin.page.ts
117
+ export class SignInPage {
118
+ constructor(public page: Page) {}
119
+ goto = () => this.page.goto('/signin');
120
+ emailField = () => this.page.getByLabel(/email/i);
121
+ passwordField = () => this.page.getByLabel(/password/i);
122
+ submit = () => this.page.getByRole('button', { name: /sign in/i });
123
+ async signIn(email: string, password: string) {
124
+ await this.emailField().fill(email);
125
+ await this.passwordField().fill(password);
126
+ await this.submit().click();
127
+ }
128
+ }
129
+ ```
130
+
131
+ Keep POMs **thin**. They abstract selectors, not flows.
132
+
133
+ ### Selectors — Hawkeye's rules
134
+
135
+ 1. `getByRole` first.
136
+ 2. `getByLabel` for forms.
137
+ 3. `getByText` for content.
138
+ 4. `data-testid` only when none of the above work (last resort).
139
+ 5. **Never** CSS classes for selectors. Class names are styling concerns; they shift.
140
+
141
+ ### Determinism
142
+
143
+ - Tests must pass 100× in a row locally. If they don't, they're flaky — fix the test or the code, never `retry: 5`.
144
+ - Use Playwright's auto-waiting (`expect(locator).toBeVisible()`). Never `setTimeout`.
145
+ - Mock time with `page.clock` when timing affects the test.
146
+
147
+ ## 4. Visual regression (selective)
148
+
149
+ ```ts
150
+ test('hero matches baseline', async ({ page }) => {
151
+ await page.goto('/');
152
+ await expect(page.getByTestId('hero')).toHaveScreenshot('hero.png', {
153
+ maxDiffPixelRatio: 0.005
154
+ });
155
+ });
156
+ ```
157
+
158
+ Use for **stable, high-value** regions (logged-out marketing, primary nav, design-system showcase). Avoid on dynamic content (timestamps, user-generated text).
159
+
160
+ ## 5. CI integration
161
+
162
+ ```yaml
163
+ # .github/workflows/test.yml (sketch)
164
+ jobs:
165
+ unit:
166
+ runs-on: ubuntu-latest
167
+ steps:
168
+ - uses: actions/checkout@v4
169
+ - uses: actions/setup-node@v4
170
+ with: { node-version: '20', cache: 'npm' }
171
+ - run: npm ci
172
+ - run: npm run test:run -- --reporter=junit --outputFile=test-results/junit.xml
173
+ - uses: actions/upload-artifact@v4
174
+ with: { name: junit, path: test-results/ }
175
+ e2e:
176
+ runs-on: ubuntu-latest
177
+ needs: unit
178
+ steps:
179
+ - uses: actions/checkout@v4
180
+ - uses: actions/setup-node@v4
181
+ with: { node-version: '20', cache: 'npm' }
182
+ - run: npm ci
183
+ - run: npx playwright install --with-deps
184
+ - run: npm run build && npm run start &
185
+ - run: npx wait-on http://localhost:3000
186
+ - run: npm run e2e
187
+ - if: failure()
188
+ uses: actions/upload-artifact@v4
189
+ with: { name: playwright-report, path: playwright-report/ }
190
+ ```
191
+
192
+ ## 6. Coverage targets (advisory)
193
+
194
+ - **Lines:** 80%+ unit/integration.
195
+ - **Branches:** 75%+ on logic modules.
196
+ - **E2E:** every critical user journey from the PRD. Not every page.
197
+
198
+ Coverage is a signal, not a goal. Hawkeye prefers one good E2E over five low-value unit tests.
199
+
200
+ ## 7. Anti-patterns Hawkeye fails fast on
201
+
202
+ - `test.skip()` left in main.
203
+ - `if (!process.env.SLOW) test.skip()` — flaky shielded as flag.
204
+ - Snapshots of entire DOM trees.
205
+ - `wait(1000)` — non-deterministic by definition.
206
+ - Mocking the unit under test (then you're testing the mock).
207
+ - Selecting by class name.
208
+
209
+ ## 8. Hand-off
210
+
211
+ For every story Tony slices, Hawkeye's `tea-design.md` declares: unit count / integration count / E2E count, fixtures needed, network mocks, environment. Shuri implements against that contract.
@@ -0,0 +1,104 @@
1
+ ---
2
+ playbook: responsive-breakpoints
3
+ owner: wize-agent-ux-designer # Mantis
4
+ applies_when: web-overlay active
5
+ status: ready
6
+ ---
7
+
8
+ # Responsive Layout — Mantis Playbook
9
+
10
+ Mobile-first by default. Container queries first; media queries when you have to. Fluid typography over stepped scales. **Design from content, not from device.**
11
+
12
+ ## 1. Breakpoint stack (mobile-first)
13
+
14
+ Token names matter more than the px value — name by intent, not device.
15
+
16
+ | Token | Min width | Rough viewport | Typical layout shift |
17
+ |---|---|---|---|
18
+ | `xs` | 0 | phone portrait | single column, full-bleed images |
19
+ | `sm` | 480px | phone landscape, phablet | wider gutters, two-col on cards |
20
+ | `md` | 768px | tablet portrait | two-column page, persistent header |
21
+ | `lg` | 1024px | tablet landscape, small laptop | three-column, sidebar appears |
22
+ | `xl` | 1280px | desktop | max canvas, multi-region pages |
23
+ | `2xl` | 1536px | large desktop | larger gutters, bigger type |
24
+
25
+ **Do not** add a breakpoint per device. Add one when the design actually breaks.
26
+
27
+ ## 2. Container queries (preferred for components)
28
+
29
+ Components should respond to **their own container width**, not the page width. Same card behaves differently in a sidebar vs hero region.
30
+
31
+ ```css
32
+ .card { container-type: inline-size; }
33
+
34
+ @container (min-width: 360px) {
35
+ .card { display: grid; grid-template-columns: 80px 1fr; }
36
+ }
37
+ ```
38
+
39
+ Use a media query only when the change is page-level (nav layout, page chrome).
40
+
41
+ ## 3. Fluid typography
42
+
43
+ Step scales jump at breakpoints; fluid scales grow continuously. Use `clamp()` to avoid both extremes.
44
+
45
+ ```css
46
+ :root {
47
+ /* clamp(min, preferred, max) */
48
+ --step-0: clamp(1rem, 0.9rem + 0.5vw, 1.125rem);
49
+ --step-1: clamp(1.125rem, 1.0rem + 0.7vw, 1.375rem);
50
+ --step-2: clamp(1.4rem, 1.2rem + 1.0vw, 1.8rem);
51
+ --step-3: clamp(1.75rem, 1.4rem + 1.7vw, 2.5rem);
52
+ --step-4: clamp(2.2rem, 1.6rem + 2.5vw, 3.5rem);
53
+ }
54
+ ```
55
+
56
+ Use **rem** for type, **em** for spacing within type contexts. Never hardcode px on body text.
57
+
58
+ ## 4. Layout primitives (use these names in design specs)
59
+
60
+ | Primitive | What it does | When |
61
+ |---|---|---|
62
+ | **Stack** | Vertical rhythm, configurable gap | Default layout. |
63
+ | **Cluster** | Inline flex with wrap | Tag rows, action groups. |
64
+ | **Switcher** | Switches row → column under threshold | Hero/sidebar pair. |
65
+ | **Sidebar** | Sticky content beside main | Lists + detail panes. |
66
+ | **Grid** | Auto-fit grid of equal cards | Card walls. |
67
+ | **Cover** | Header / centered content / footer | Landing sections. |
68
+ | **Frame** | Aspect-ratio container | Videos, hero images. |
69
+
70
+ These map directly to CSS — keep specs talking about primitives, not pixels.
71
+
72
+ ## 5. Image strategy
73
+
74
+ - **Always set `width` and `height` attributes** (prevents CLS).
75
+ - Serve **AVIF → WebP → JPEG/PNG** via `<picture>`.
76
+ - Use `srcset` + `sizes` for art-direction and density.
77
+ - Lazy-load below-the-fold images: `loading="lazy" decoding="async"`.
78
+ - Hero images: preload (`<link rel="preload" as="image">`) the LCP candidate.
79
+
80
+ ## 6. Touch + pointer
81
+
82
+ - Touch targets: **24×24 CSS px minimum** (WCAG 2.5.8); promote to 44×44 for primary CTAs.
83
+ - Hover states must have a non-hover equivalent (touch users don't hover).
84
+ - Use `@media (hover: hover)` to gate hover-only affordances.
85
+
86
+ ## 7. Dark mode
87
+
88
+ - Honor `prefers-color-scheme` by default.
89
+ - Provide an in-app override stored in `localStorage`.
90
+ - Token-driven: every color comes from a semantic token (`--surface`, `--text`, `--accent`), not a raw hex.
91
+ - Test contrast in **both** modes.
92
+
93
+ ## 8. Motion
94
+
95
+ - Honor `prefers-reduced-motion`. Default to motion only when the user opted in or the system says it's OK.
96
+ - Durations: 100ms (micro) / 200ms (transition) / 300ms (page-level). Anything > 300ms feels slow.
97
+
98
+ ## 9. Smoke walk before sign-off
99
+
100
+ Walk every page at: **320px**, **768px**, **1024px**, **1440px**, **portrait/landscape**, plus **200% zoom**. No horizontal scroll. No clipped content. No focus loss.
101
+
102
+ ## 10. Hand-off to Shuri
103
+
104
+ Specs should reference tokens (`--space-3`, `--step-2`, `--surface-1`) — not raw values. Tony picks the implementation (Tailwind, CSS modules, Vanilla Extract); Shuri implements against the tokens Mantis defined in `.wize/solutioning/design-system/tokens.json`.
@@ -0,0 +1,114 @@
1
+ ---
2
+ playbook: semantic-html
3
+ owner: wize-agent-ux-designer # Mantis (with Tony for component patterns)
4
+ applies_when: web-overlay active
5
+ status: ready
6
+ ---
7
+
8
+ # Semantic HTML — Mantis Playbook
9
+
10
+ The first rule: **use the right element**. ARIA is for when the platform doesn't ship the element you need; it is not a substitute for `<button>`.
11
+
12
+ ## 1. Landmarks (one per page, usually)
13
+
14
+ | Element | Role | Purpose |
15
+ |---|---|---|
16
+ | `<header>` (top-level) | `banner` | Site/app chrome. |
17
+ | `<nav>` | `navigation` | Primary nav. Label multiples with `aria-label`. |
18
+ | `<main>` | `main` | Unique main content. One per page. |
19
+ | `<aside>` | `complementary` | Related-to-main content. |
20
+ | `<footer>` (top-level) | `contentinfo` | Site/app footer. |
21
+ | `<search>` (HTML 2024) | `search` | Search regions. |
22
+
23
+ Screen reader users navigate by landmarks. Use them; label duplicates.
24
+
25
+ ## 2. Headings
26
+
27
+ - One `<h1>` per page (the page topic).
28
+ - Headings define **structure**, not size. Never skip levels for style — adjust style with CSS.
29
+ - Sections that need their own heading use `<section>` with `aria-labelledby` pointing at the heading.
30
+
31
+ ## 3. The 12 elements you must reach for first
32
+
33
+ | Need | Element | Why not the alternative |
34
+ |---|---|---|
35
+ | Clickable action | `<button>` | `<div onclick>` has no keyboard, no focus, no role. |
36
+ | Navigation link | `<a href>` | `<button>` doesn't open URLs; routers should still hit `<a>`. |
37
+ | Form field | `<input>`/`<textarea>`/`<select>` with `<label>` | DIY inputs lose autofill, IME, mobile keyboards. |
38
+ | Yes/No state | `<input type="checkbox">` | Toggles aren't divs. |
39
+ | List of things | `<ul>`/`<ol>`/`<li>` | Readers announce count and position. |
40
+ | Tabular data | `<table>` with `<thead>`/`<th scope>` | Real semantics for real data. (Not for layout.) |
41
+ | Disclosure | `<details>`/`<summary>` | Built-in keyboard + state. |
42
+ | Modal | `<dialog>` with `.showModal()` | Focus trap + Escape are free. |
43
+ | Tooltips | `<button aria-describedby>` + visible region | Hover-only tooltips fail touch + keyboard. |
44
+ | Date input | `<input type="date">` (when locale-OK) | Native pickers feel right on mobile. |
45
+ | Range | `<input type="range">` | Keyboard arrows + ARIA are built in. |
46
+ | Progress | `<progress>` / `<meter>` | Semantic value + max. |
47
+
48
+ ## 4. Forms — the contract
49
+
50
+ ```html
51
+ <form>
52
+ <div class="field">
53
+ <label for="email">Email address</label>
54
+ <input id="email" name="email" type="email" autocomplete="email"
55
+ required aria-describedby="email-help email-error">
56
+ <p id="email-help">We'll never share your email.</p>
57
+ <p id="email-error" role="alert" hidden>Please enter a valid email.</p>
58
+ </div>
59
+ <button type="submit">Sign up</button>
60
+ </form>
61
+ ```
62
+
63
+ Rules:
64
+ 1. Label is **always visible** (not placeholder-only).
65
+ 2. `autocomplete` on every input that has a meaningful value.
66
+ 3. `required` + a `aria-describedby` error region.
67
+ 4. Error region uses `role="alert"` or `aria-live="assertive"` only at submit; live regions on every keystroke are noisy.
68
+ 5. Submit triggers via `Enter` automatically when inside `<form>` — keep it that way.
69
+
70
+ ## 5. ARIA — when (rarely) to use it
71
+
72
+ Use ARIA only when:
73
+ - You can't use the native element (rare).
74
+ - You're building a widget HTML doesn't ship (combobox, treeview, slider with custom track).
75
+
76
+ The five ARIA rules:
77
+ 1. **Don't** use ARIA if native works.
78
+ 2. Don't change native semantics with `role` unless you must.
79
+ 3. All ARIA controls must be keyboard accessible.
80
+ 4. Don't use `role="presentation"` on focusable elements.
81
+ 5. Interactive elements must have an accessible name.
82
+
83
+ ## 6. Common widget patterns (with minimum ARIA)
84
+
85
+ | Widget | Native option | Custom shape (minimum) |
86
+ |---|---|---|
87
+ | Accordion | `<details>`/`<summary>` | `<button aria-expanded aria-controls="id">` + `<div id="id" hidden>`. |
88
+ | Tabs | — (no native) | `role="tablist"` > `role="tab" aria-selected aria-controls` ; `role="tabpanel" aria-labelledby`. |
89
+ | Combobox | `<input list>` for simple | ARIA 1.2 combobox pattern; non-trivial. Use a tested lib. |
90
+ | Toggle | `<input type="checkbox">` with switch styling | `<button role="switch" aria-checked>`. |
91
+ | Toast | — | `role="status" aria-live="polite"` (info); `role="alert"` (errors). |
92
+ | Tooltip | `title` (limited) | `<button aria-describedby="tip">` + `<div role="tooltip" id="tip">`. |
93
+ | Modal | `<dialog>` | `role="dialog" aria-modal="true" aria-labelledby`; focus trap; restore focus on close. |
94
+ | Menu | — | `role="menu"` > `role="menuitem"` ; arrow keys + Escape; rarely needed for app UIs. |
95
+
96
+ ## 7. Lint and audit
97
+
98
+ - **eslint-plugin-jsx-a11y** for React/JSX projects.
99
+ - **eslint-plugin-vuejs-accessibility** for Vue.
100
+ - **axe-core** at runtime (browser ext + CI).
101
+ - **Manual screen-reader walk** on critical flows before each major release.
102
+
103
+ ## 8. Don'ts (most common in the wild)
104
+
105
+ - `<div onclick>` instead of `<button>`.
106
+ - `outline: none` on `:focus` without replacement.
107
+ - Placeholders as labels.
108
+ - Icon-only buttons without `aria-label` (or visible text).
109
+ - `aria-label` on a `<div>` that isn't interactive (does nothing).
110
+ - `tabindex` > 0 (breaks focus order).
111
+
112
+ ## 9. Hand-off
113
+
114
+ When Mantis writes UX specs, every interactive element is named with its **HTML element name**, not its style ("button: Continue", not "blue rectangle"). This forces semantic discussion upstream of CSS.
@@ -0,0 +1,97 @@
1
+ ---
2
+ playbook: wcag-aa
3
+ owner: wize-agent-ux-designer # Mantis
4
+ applies_when: web-overlay active
5
+ status: ready
6
+ ---
7
+
8
+ # WCAG 2.2 AA — Mantis Playbook
9
+
10
+ Use this when you're shaping UX and need a concrete accessibility floor. Treat AA as the **minimum**; promote items to AAA when the product audience demands it (e.g., gov, healthcare).
11
+
12
+ ## 1. Quick principles (POUR)
13
+
14
+ | Principle | What it means | Most-broken in the wild |
15
+ |---|---|---|
16
+ | **Perceivable** | Content can be seen/heard by everyone. | Color contrast, alt text. |
17
+ | **Operable** | UI works with keyboard + assistive tech. | Focus traps, no-skip nav. |
18
+ | **Understandable** | Predictable, error-tolerant. | Form errors with no explanation. |
19
+ | **Robust** | Works across browsers and ATs. | Custom controls without ARIA roles. |
20
+
21
+ ## 2. Mandatory AA checklist for every screen
22
+
23
+ ### Perceivable
24
+ - [ ] Text contrast **≥ 4.5:1** (normal) / **≥ 3:1** (large ≥ 18pt / 14pt-bold).
25
+ - [ ] Non-text UI (icons, focus rings, form borders) contrast **≥ 3:1** against background.
26
+ - [ ] Every `<img>` has `alt`; decorative images use `alt=""` + `aria-hidden`.
27
+ - [ ] Video has captions; audio-only content has a transcript.
28
+ - [ ] Don't convey meaning by color alone (error = red + icon + label).
29
+ - [ ] Page works at **200% zoom** without loss of content or function.
30
+
31
+ ### Operable
32
+ - [ ] **Every interactive element is reachable and operable by keyboard.** Tab order matches visual order.
33
+ - [ ] Visible focus indicator (≥ 3:1 contrast) on **all** focusable elements. Don't override with `outline:none` without replacement.
34
+ - [ ] "Skip to main content" link is the first focusable element on the page.
35
+ - [ ] No keyboard trap. `Esc` closes modals; modals trap focus *within* the modal until closed.
36
+ - [ ] Touch targets ≥ **24×24 CSS px** (WCAG 2.2 SC 2.5.8). Promote to 44×44 for primary actions.
37
+ - [ ] No motion-triggered actions without a non-motion fallback.
38
+ - [ ] User can pause/stop/hide content that auto-plays > 5s.
39
+
40
+ ### Understandable
41
+ - [ ] `lang` attribute set on `<html>` (and on inline lang switches).
42
+ - [ ] Form fields have **persistent, visible labels** (not placeholder-only).
43
+ - [ ] Required fields marked with text + visual cue (not asterisk alone).
44
+ - [ ] Error messages: identify the field, describe the fix, link back to the input.
45
+ - [ ] Consistent navigation across pages (same links in same order).
46
+
47
+ ### Robust
48
+ - [ ] Use semantic HTML before reaching for ARIA. (See `semantic-html.md`.)
49
+ - [ ] Custom widgets have proper ARIA role + state + value.
50
+ - [ ] No duplicate IDs on a page.
51
+ - [ ] Status messages (toasts, async results) announced via `role="status"` or `aria-live="polite"`.
52
+
53
+ ## 3. WCAG 2.2 specifics (newer SCs to remember)
54
+
55
+ | SC | Topic | Practical implication |
56
+ |---|---|---|
57
+ | 2.4.11 | Focus not obscured (minimum) | Sticky headers/footers must not cover the focused element. |
58
+ | 2.5.7 | Dragging movements | Anything drag-only needs a non-drag alternative. |
59
+ | 2.5.8 | Target size (minimum) | 24×24 CSS px (excluding ample spacing). |
60
+ | 3.2.6 | Consistent help | Help link in the same relative location across pages. |
61
+ | 3.3.7 | Redundant entry | Don't ask the user to retype info already submitted in the same session. |
62
+ | 3.3.8 | Accessible authentication | Don't require cognitive function tests for login (no "type these letters" puzzles). |
63
+
64
+ ## 4. Tools to run (every PR that touches UI)
65
+
66
+ | Tool | What it catches | Notes |
67
+ |---|---|---|
68
+ | **axe DevTools** (browser ext) | ~57% of WCAG issues automatically. | First line. Free. |
69
+ | **Lighthouse → Accessibility** | Subset of axe; ships in Chrome. | CI-friendly. |
70
+ | **pa11y / pa11y-ci** | Headless audit in pipelines. | Add to PR check. |
71
+ | **NVDA** (Windows) or **VoiceOver** (Mac/iOS) | Manual screen reader pass. | At least the critical flows. |
72
+ | **Keyboard-only walk** | Focus order, traps, hidden controls. | 5 minutes per screen. |
73
+
74
+ ## 5. Common patterns Mantis ships with
75
+
76
+ - **Skip link:** first focusable element, hidden visually until focused.
77
+ - **Form pattern:** `<label>` always visible, error region with `aria-live="polite"` next to each input.
78
+ - **Modal:** `role="dialog" aria-modal="true"`, focus trapped, focus restored to opener on close.
79
+ - **Disclosure/Accordion:** `<button aria-expanded>` toggling a `<div id>`; not raw `<div onclick>`.
80
+ - **Toast:** `role="status"`, dismiss-on-press + auto-dismiss after ≥ 10s for readers.
81
+ - **Async loading:** announce via `aria-live="polite"`, never silent.
82
+
83
+ ## 6. Hand-off note for Tony and Shuri
84
+
85
+ Tony, when picking a component library, evaluate it on:
86
+ - Real focus indicators (not just outline override).
87
+ - Form components that ship labels + error regions out of the box.
88
+ - Modals that handle focus return.
89
+
90
+ Shuri, when implementing, **never delete the focus ring** without replacing it with a custom one of equal contrast. Every PR touching UI runs axe + a keyboard walk.
91
+
92
+ ## 7. When AA isn't enough
93
+
94
+ Promote to AAA when:
95
+ - Product targets users with low vision (consider 7:1 contrast).
96
+ - Public-sector or healthcare deployment.
97
+ - Compliance (Section 508, EN 301 549, ADA).
@@ -0,0 +1,140 @@
1
+ ---
2
+ playbook: web-perf-budgets
3
+ owner: wize-agent-test-architect # Hawkeye (with Tony on stack picks)
4
+ applies_when: web-overlay active
5
+ status: ready
6
+ ---
7
+
8
+ # Web Performance Budgets — Hawkeye Playbook
9
+
10
+ Budgets are decisions, not measurements. Set them once, enforce them on every PR.
11
+
12
+ ## 1. Core Web Vitals — the floor
13
+
14
+ | Metric | Good | Needs work | Poor |
15
+ |---|---|---|---|
16
+ | **LCP** (Largest Contentful Paint) | ≤ 2.5s | 2.5–4.0s | > 4.0s |
17
+ | **INP** (Interaction to Next Paint) | ≤ 200ms | 200–500ms | > 500ms |
18
+ | **CLS** (Cumulative Layout Shift) | ≤ 0.1 | 0.1–0.25 | > 0.25 |
19
+
20
+ Measure on **mobile, slow 4G, mid-range device** — not your laptop on fiber.
21
+
22
+ ## 2. Baseline budgets (mid-range mobile, 3G fast)
23
+
24
+ | Resource | Budget | How to enforce |
25
+ |---|---|---|
26
+ | **Total transferred** (initial route) | ≤ 200 KB compressed | `lighthouse-ci` budget. |
27
+ | **JS** | ≤ 100 KB compressed (≤ 300 KB uncompressed) | Bundle analyzer + CI check. |
28
+ | **CSS** | ≤ 30 KB | Critical-CSS inlined; rest deferred. |
29
+ | **Images (above the fold)** | ≤ 100 KB total | AVIF/WebP, `<picture>`. |
30
+ | **Fonts** | ≤ 30 KB (2 weights subset) | `font-display: swap`, preload. |
31
+ | **Third-party requests** | ≤ 5 | Audit on every release. |
32
+
33
+ These are starting points. Tony tunes them per project after the PRD nails the audience and target device.
34
+
35
+ ## 3. JS budget — what eats it
36
+
37
+ - Framework runtime (React: ~50 KB gz, Vue: ~30 KB, Svelte: 0 KB at runtime).
38
+ - Router.
39
+ - State manager.
40
+ - Date library (date-fns ≪ moment).
41
+ - Form library.
42
+ - UI components from `node_modules`.
43
+
44
+ Audit with `npx vite-bundle-visualizer` (Vite) or `next-bundle-analyzer` (Next). Remove the top 3 by size every quarter.
45
+
46
+ ## 4. Image strategy
47
+
48
+ 1. **Format:** AVIF first → WebP → JPEG/PNG fallback.
49
+ 2. **Width:** never serve more than 2× the rendered CSS width.
50
+ 3. **Lazy:** below-the-fold = `loading="lazy"`. LCP image = `<link rel="preload" as="image">`.
51
+ 4. **Dimensions:** always set `width` and `height` HTML attrs to prevent CLS.
52
+ 5. **Responsive:** `<picture>` with `srcset` + `sizes`, or a framework-native `<Image>`.
53
+
54
+ ## 5. Font strategy
55
+
56
+ - Self-host. Don't fetch from CDN for performance work.
57
+ - Subset to the characters you actually use (Glyphhanger / fonttools).
58
+ - `font-display: swap` so text appears instantly.
59
+ - Preload the primary font; defer secondaries.
60
+ - Cap variable fonts at 2 axes.
61
+
62
+ ## 6. Third parties (the silent killers)
63
+
64
+ For every third-party script, answer:
65
+
66
+ 1. **What user value does it deliver?**
67
+ 2. **Can it load after `load` event?** (analytics, A/B, chat)
68
+ 3. **Can it load only on interaction?** (chat widgets, video embeds)
69
+ 4. **Has it been replaced by a smaller alternative?** (e.g., Plausible vs GA)
70
+ 5. **Is it sandboxed in a `<iframe>` or web worker?**
71
+
72
+ Audit list in `.wize/planning/web/perf-budget.md`. Remove or defer one every release until they all justify their bytes.
73
+
74
+ ## 7. Critical rendering path
75
+
76
+ ```
77
+ <head>
78
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
79
+ <link rel="preload" as="image" href="/hero.avif" type="image/avif">
80
+ <link rel="preload" as="font" type="font/woff2" href="/fonts/Inter.var.woff2" crossorigin>
81
+ <style>/* critical above-the-fold CSS, ≤ 14 KB */</style>
82
+ <link rel="stylesheet" href="/main.css" media="print" onload="this.media='all'">
83
+ </head>
84
+ ```
85
+
86
+ - Critical CSS inline (≤ 14 KB so the first packet contains it).
87
+ - Everything else deferred or preloaded.
88
+ - No render-blocking external scripts above content.
89
+
90
+ ## 8. INP — interaction responsiveness
91
+
92
+ INP measures **the slowest interaction** a user has across the session, not the average. One bad input handler ruins your score.
93
+
94
+ Hot fixes:
95
+ 1. Yield to the main thread inside long handlers (`await scheduler.yield()` or `setTimeout(0)`).
96
+ 2. Move heavy work to a worker (`react-server-components`, `comlink`).
97
+ 3. Debounce typing-driven recalculations.
98
+ 4. Use `<input type=*` natives; custom inputs are slower.
99
+ 5. Defer hydration (Astro Islands, Qwik, React Server Components).
100
+
101
+ ## 9. Build-time enforcement
102
+
103
+ ```jsonc
104
+ // lighthouserc.json (lighthouse-ci)
105
+ {
106
+ "ci": {
107
+ "collect": {
108
+ "url": ["https://staging.example.com/", "https://staging.example.com/dashboard"],
109
+ "settings": { "preset": "perf" }
110
+ },
111
+ "assert": {
112
+ "assertions": {
113
+ "categories:performance": ["error", { "minScore": 0.9 }],
114
+ "first-contentful-paint": ["warn", { "maxNumericValue": 1800 }],
115
+ "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
116
+ "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
117
+ "total-byte-weight": ["error", { "maxNumericValue": 250000 }]
118
+ }
119
+ }
120
+ }
121
+ }
122
+ ```
123
+
124
+ Wire into CI; gate merges on perf score.
125
+
126
+ ## 10. Field measurement
127
+
128
+ Lab tests catch obvious regressions; field tests catch the truth. Add a Web Vitals beacon:
129
+
130
+ ```ts
131
+ import { onLCP, onINP, onCLS } from 'web-vitals';
132
+ const beacon = (m: any) => navigator.sendBeacon('/v', JSON.stringify(m));
133
+ onLCP(beacon); onINP(beacon); onCLS(beacon);
134
+ ```
135
+
136
+ Aggregate by route + device class + connection in your analytics. Look at p75, not avg.
137
+
138
+ ## 11. Hand-off
139
+
140
+ For every epic, Hawkeye attaches an NFR report (`tea/nfr/{epic}.md`) with current vs target Core Web Vitals. Fury sets the targets in `nfr-principles.md`. Tony picks the stack mindful of the runtime cost ahead of time, not afterward.