Add workflow infrastructure — ESLint, Prettier, Husky, Vitest, 7 new skills

Phase 1: Session log archived (1096→91 lines), D031 token access convention
Phase 2: ESLint v9 + Prettier + jsx-a11y, initial config and lint fixes
Phase 3: 7 new skills (polish, harden, normalize, clarify, typeset, quieter, adapt)
         + Vercel reference docs, updated audit/review-component refs
Phase 4: Husky + lint-staged pre-commit hooks, preflight updated to 8 checks
Phase 5: Vitest + Testing Library + /write-tests skill

- Badge.tsx colour maps unified to CSS variables (D031)
- 5 empty interface→type alias fixes (Switch, Radio, Divider, IconButton, Link)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-27 16:41:57 +11:00
parent c5bfeaee2f
commit aa7cdeecf0
33 changed files with 7685 additions and 1088 deletions

View File

@@ -0,0 +1,130 @@
---
name: adapt
description: Responsive adaptation review — checks touch targets, overflow, text scaling, content reflow, and mobile spacing across breakpoints.
user-invocable: true
argument-hint: "[component or area to check]"
---
Run a responsive design review on a component or area, then **fix issues found**. This is assessment AND fix — diagnose breakpoint problems, touch target failures, and mobile usability issues, then apply corrections.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA responsive conventions and breakpoints
2. Read the target component/area source files and stories
3. Reference `docs/reference/impeccable/responsive-design.md` for detailed responsive guidelines
**FA context reminder**: Many Funeral Arranger users are older adults, potentially on older or budget devices, and may be using the platform on mobile during a difficult time. Touch interactions must be generous. Layouts must not break on small screens. Nothing should require precise fine-motor control or hover-only interactions. The platform must work on a 5-year-old Android phone on a slow connection just as well as on a modern desktop.
## Diagnostic Checks
Work through each check systematically. For each issue found, note the problem, then fix it.
### 1. Touch Targets
- All interactive elements (buttons, links, form controls, toggles) must have a minimum touch target of 44x44px
- Check `padding`, `min-height`, and `min-width` on clickable elements — the visual size can be smaller if the tap area is padded out
- Text links in body copy need adequate line-height or padding to create 44px+ tap areas
- Adjacent touch targets must have sufficient spacing (at least 8px gap) to prevent mis-taps
- Icon-only buttons are especially prone to being too small — verify dimensions include padding
- This is **critical** for FA's audience — elderly users and users under emotional stress have reduced fine-motor precision
### 2. Horizontal Overflow
- No horizontal scrolling on viewports as narrow as 320px (iPhone SE, older Android devices)
- Check for fixed widths (`width: 400px`, `min-width: 500px`) that exceed narrow viewports
- Check for content that breaks out of containers (long words, URLs, email addresses without `word-break` or `overflow-wrap`)
- Tables must adapt on mobile — use responsive patterns (card layout, horizontal scroll with visual indicator, or progressive disclosure)
- Images must have `max-width: 100%` or equivalent constraint
- Check for `flex-shrink: 0` or `flex: none` on elements that should be allowed to shrink
### 3. Text Readability at Mobile Sizes
- Body text minimum 14px on mobile (16px preferred)
- Use `rem` units for font sizes so text respects browser zoom/accessibility settings
- Never use `user-scalable=no` in viewport meta — this breaks accessibility
- Layouts must not break at 200% browser zoom (WCAG requirement)
- Small/caption text should remain legible — minimum 12px
- Check that heading sizes scale down appropriately on mobile (a 48px desktop heading should not stay 48px on a 320px screen)
### 4. Content Reflow
- Content should reflow logically between breakpoints, not just shrink
- Multi-column layouts should collapse to single column on mobile
- Side-by-side arrangements (image + text, icon + label) should stack vertically when space is constrained
- Check `flex-direction` and `grid-template-columns` for responsive adjustments
- Navigation should adapt: full horizontal on desktop, hamburger/drawer on mobile
- Cards in a grid should reduce column count at narrow widths, not just shrink cards
### 5. Hover-Only Interactions
- No functionality should require hover — touch users cannot hover
- Tooltips that appear only on hover need a touch alternative (tap to show, or visible by default on mobile)
- Hover-revealed actions (edit buttons, delete icons) must have an alternative on touch devices
- Use `@media (hover: hover)` for hover enhancements, with fallback for `@media (hover: none)`
- Dropdown menus triggered by hover must also work on tap
- Check for CSS `:hover` styles that hide/show content without a touch-friendly fallback
### 6. Spacing Adjustments Per Breakpoint
- Padding should be tighter on mobile (16px) and more generous on desktop (24-48px)
- Section spacing should compress on mobile to keep content scannable without excessive scrolling
- Card padding should adapt — desktop cards can be spacious, mobile cards should be compact but not cramped
- Check that spacing uses theme.spacing() or responsive values, not fixed values that work only at one breakpoint
- Container max-widths should be set appropriately for each breakpoint
### 7. Image and Asset Sizing
- Images should have responsive sizing (`max-width: 100%`, `height: auto`)
- Consider `loading="lazy"` for images below the fold (important for FA users on slow connections)
- Decorative images can be hidden on mobile if they add no informational value
- Avatar/icon sizes should be appropriate for mobile — not too large (wasting space) or too small (illegible)
- Check for background images that might not display well on small screens
## Fix Process
For each issue found:
1. **Identify** the file, line, and current value
2. **Test mentally at 320px, 768px, and 1280px** — does the fix work across all three?
3. **Apply the fix** — use responsive utilities (MUI breakpoints, media queries, responsive props)
4. **Verify** the fix does not introduce new issues at other breakpoints
5. **Prefer CSS/theme solutions** over JavaScript breakpoint detection where possible
## Report Format
After fixing, present a summary:
### Responsive Health Summary
| Check | Status | Issues Found | Issues Fixed |
|-------|--------|-------------|-------------|
| Touch Targets (44px min) | pass/warn/fail | N | N |
| Horizontal Overflow (320px) | pass/warn/fail | N | N |
| Text Readability (mobile) | pass/warn/fail | N | N |
| Content Reflow | pass/warn/fail | N | N |
| Hover-Only Interactions | pass/warn/fail | N | N |
| Breakpoint Spacing | pass/warn/fail | N | N |
| Image/Asset Sizing | pass/warn/fail | N | N |
### Breakpoint Walkthrough
Brief assessment of the component at each key breakpoint:
- **320px** (small phone): Does it fit? Is it usable?
- **768px** (tablet): Does the layout adapt sensibly?
- **1280px** (desktop): Does it use the available space well?
### Changes Made
For each fix applied:
- **What changed**: Brief description
- **File**: Path and relevant line(s)
- **Before/After**: The old and new values
- **Breakpoint affected**: Which viewport size(s) this fix targets
### Remaining Concerns
Any issues that need design input, Storybook viewport testing, or are outside the scope of a responsive pass.
**NEVER**: Use fixed pixel widths for containers. Add `user-scalable=no` to viewport meta. Rely on hover for essential functionality. Assume all users have modern large-screen devices. Ignore the 320px viewport — real people use iPhone SE and budget Android phones. Let touch targets fall below 44px.

View File

@@ -14,6 +14,7 @@ Run systematic **technical** quality checks on a component or area and generate
1. Read `docs/design-system.md` for FA design conventions
2. Read the component/area source files
3. Reference `docs/reference/impeccable/` for detailed design guidelines when scoring
4. Reference `docs/reference/vercel/web-design-guidelines.md` for additional WCAG/accessibility rules (optional, for deeper a11y scoring)
## Diagnostic Scan

View File

@@ -0,0 +1,150 @@
---
name: clarify
description: Information hierarchy improvement — evaluates whether primary actions are visible, decision points are clear, content grouping is logical, and labels are unambiguous.
user-invocable: true
argument-hint: "[component or area to clarify]"
---
Evaluate and fix information hierarchy, cognitive load, and decision clarity. Unlike audit/critique which only assess, **clarify finds AND fixes issues**. This skill is critical for flows, forms, and any interface where users must make decisions.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA design conventions
2. Read `docs/memory/decisions-log.md` for design rationale
3. Read the target component/area source files and stories
4. Reference `docs/reference/impeccable/cognitive-load.md` for the 8-item checklist and violation patterns
5. Reference `docs/reference/impeccable/heuristics-scoring.md` for scoring criteria
**FA context**: Funeral Arranger serves families in grief or distress. Cognitive capacity is reduced under grief — working memory shrinks, decision fatigue sets in faster, and tolerance for ambiguity drops to near zero. Clarity is not a nice-to-have, it is the primary design requirement. No jargon. No ambiguity. No unnecessary choices. Every screen should answer: "What am I looking at? What should I do? What happens next?"
## Phase 1: Cognitive Load Assessment
Run the 8-item checklist from `docs/reference/impeccable/cognitive-load.md` against the target:
| # | Check | Pass/Fail | Notes |
|---|-------|-----------|-------|
| 1 | **Single focus**: Can the user complete their primary task without distraction? | | |
| 2 | **Chunking**: Is information in digestible groups (<=4 items per group)? | | |
| 3 | **Grouping**: Are related items visually grouped (proximity, borders, shared background)? | | |
| 4 | **Visual hierarchy**: Is it immediately clear what is most important? | | |
| 5 | **One thing at a time**: Can the user focus on one decision before the next? | | |
| 6 | **Minimal choices**: Are decisions simplified (<=4 visible options at any decision point)? | | |
| 7 | **Working memory**: Does the user need to remember info from a previous screen? | | |
| 8 | **Progressive disclosure**: Is complexity revealed only when needed? | | |
**Scoring**: 0-1 failures = low cognitive load (good). 2-3 = moderate (address soon). 4+ = high cognitive load (critical fix needed).
## Phase 2: Clarity Checks
Evaluate each dimension. **Fix issues as you find them.**
### 1. Primary Action Visibility
The most important action on any screen should be identifiable within 2 seconds.
- [ ] **One primary action**: There is exactly one visually dominant CTA per view
- [ ] **Visual weight**: The primary action has the strongest visual weight (size, colour, contrast)
- [ ] **Position**: The primary action is in a predictable location (bottom of form, right of button group)
- [ ] **No competition**: Secondary actions are visually subordinate (outlined or text style, not filled)
- [ ] **Label clarity**: The CTA label says what will happen ("Continue to payment" not "Next" or "Submit")
**FA-specific**: For stepped flows (arrangement, funeral finder), the primary CTA must always be visible without scrolling. Smart defaults should pre-fill where possible to reduce friction.
### 2. Decision Point Clarity
At every point where the user must choose:
- [ ] **<=4 options visible**: If more exist, group under categories or use progressive disclosure
- [ ] **Recommended option**: When appropriate, visually highlight the recommended choice
- [ ] **Clear labels**: Every option has an unambiguous label — the user should never wonder "what does this mean?"
- [ ] **Consequence preview**: The user can see what choosing each option will lead to
- [ ] **Reversibility signal**: If a choice can be changed later, say so ("You can change this later")
**FA-specific**: Pricing decisions are high-anxiety. Show what is included, not just the price. For service options, group by category (venue, ceremony, extras) rather than showing a flat list.
### 3. Content Grouping
Related information should be visually grouped:
- [ ] **Proximity**: Related items are physically close together
- [ ] **Separation**: Unrelated groups have clear visual separation (whitespace, dividers, card boundaries)
- [ ] **Labels**: Each group has a clear heading that describes its contents
- [ ] **Consistency**: Same type of grouping used for same type of content throughout
### 4. Label Clarity
For every label, heading, and piece of instructional text:
- [ ] **Unambiguous**: Could not be misinterpreted by a reasonable person
- [ ] **Action-oriented**: Buttons describe what they do, not what they are
- [ ] **Jargon-free**: No funeral industry jargon unless unavoidable (and defined inline if so)
- [ ] **Consistent**: Same concept uses the same word everywhere (not "funeral" in one place and "service" in another for the same thing)
- [ ] **Concise**: Labels are as short as possible while remaining clear
**FA-specific**: Avoid euphemistic language that creates ambiguity. "Funeral service" is clearer than "farewell ceremony." Be direct but warm.
### 5. Information Hierarchy
The visual presentation should match the importance of information:
- [ ] **Reading order**: The most important information comes first (top, left)
- [ ] **Size signals importance**: Larger text = more important
- [ ] **Colour signals importance**: Brand/accent colour draws the eye to what matters
- [ ] **Redundancy eliminated**: No information is repeated without purpose
- [ ] **Noise removed**: Every element earns its place — nothing decorative without function
### 6. Navigation Clarity
The user should always know where they are and how to move:
- [ ] **Current location visible**: Active states, breadcrumbs, or step indicators show position
- [ ] **Next step obvious**: The path forward is clear without thinking
- [ ] **Back path clear**: The user can always go back without losing work
- [ ] **Progress visible**: Multi-step flows show progress (step 2 of 4)
## Phase 3: Cognitive Load Violations Scan
Check for common cognitive load violations (from `docs/reference/impeccable/cognitive-load.md`):
1. **Wall of Options** — 10+ choices with no hierarchy. **Fix**: Group, highlight recommended, use progressive disclosure.
2. **Memory Bridge** — Must remember info from step 1 to complete step 3. **Fix**: Keep context visible or repeat it.
3. **Hidden Navigation** — Must build a mental map. **Fix**: Show current location always.
4. **Jargon Barrier** — Technical/domain language forces translation effort. **Fix**: Plain language, define terms inline.
5. **Visual Noise Floor** — Every element has the same visual weight. **Fix**: Clear hierarchy — one primary, 2-3 secondary, rest muted.
6. **Inconsistent Pattern** — Similar actions work differently in different places. **Fix**: Standardise interaction patterns.
7. **Multi-Task Demand** — Must process multiple simultaneous inputs. **Fix**: Sequence the steps.
8. **Context Switch** — Must jump between screens to gather info for one decision. **Fix**: Co-locate information.
## Clarification Report
After fixing issues, present:
### Cognitive Load Score
**Checklist failures**: X/8
**Rating**: Low / Moderate / High
### What Was Clarified
For each fix:
- **Area**: What was changed
- **Problem**: What was unclear or overloaded
- **Fix**: What was done to clarify
- **Rationale**: Why this improves the experience for FA's users
### Clarity Wins
Note areas that are already well-structured — reinforce good patterns.
### Remaining Concerns
Note any structural clarity issues that require architectural changes beyond this skill's scope.
**NEVER**:
- Add jargon or technical language — always simplify
- Present more than 4 options at a decision point without grouping
- Remove information that users need — clarify means restructure, not delete
- Change component behaviour or API — only change presentation, labels, grouping, and hierarchy
- Use ambiguous labels ("Submit", "Click here", "Next") when specific labels are possible

View File

@@ -0,0 +1,135 @@
---
name: harden
description: Edge case and robustness review — checks error states, empty states, loading states, boundary values, and disabled interactions. Critical for forms and arrangement flows.
user-invocable: true
argument-hint: "[component or area to harden]"
---
Systematically review and fix edge cases, error states, and boundary conditions. Unlike audit/critique which only assess, **harden finds AND fixes issues**. This skill is especially critical for forms, stepped flows, and anything involving user input.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA design conventions
2. Read `docs/memory/decisions-log.md` for design rationale (especially D024 on error states)
3. Read the target component/area source files and stories
4. Reference `docs/reference/impeccable/interaction-design.md` for interactive state requirements
5. Reference `docs/reference/impeccable/cognitive-load.md` for cognitive load under stress
6. Reference `docs/conventions/component-conventions.md` for structural rules
**FA context**: Funeral Arranger serves families in grief or distress. When something goes wrong, the interface must be gentle and guiding, never blaming or alarming. Error states use copper tones, not red (D024). Empty states should guide toward action, not leave users stranded. Loading states should reduce perceived wait time — grieving users have less patience for ambiguity.
## Hardening Checklist
Work through each category. **Fix issues as you find them** — do not just document.
### 1. Error States
For every input, form field, and async operation:
- [ ] **Error is visible**: Error message appears near the source of the problem
- [ ] **Error is gentle**: Uses copper tones (`feedback.error`), not aggressive red. Per D024, labels stay neutral
- [ ] **Error is specific**: Identifies the exact problem ("Please enter a valid email" not "Invalid input")
- [ ] **Error is actionable**: Tells the user how to fix it
- [ ] **Error is accessible**: Connected via `aria-describedby`, announced to screen readers
- [ ] **Error does not destroy work**: Form data is preserved when validation fails
- [ ] **Error timing**: Validates on blur for individual fields, on submit for cross-field validation
- [ ] **Network errors**: Graceful message for failed API calls with retry option
- [ ] **Unexpected errors**: Catch-all error boundary that does not show a blank screen
**FA-specific**: Error copy should never blame the user. Use passive voice for errors ("A valid email is needed" not "You entered an invalid email"). Offer help where possible.
### 2. Empty States
For every list, collection, search result, and data display:
- [ ] **Empty state exists**: Not just blank space or a bare container
- [ ] **Empty state guides**: Tells the user what this area is for and how to populate it
- [ ] **Empty state has a CTA**: Primary action to add/create/search is visible
- [ ] **Empty state feels warm**: Consistent with FA's supportive tone
- [ ] **Empty state is distinct**: Clearly different from loading state — user should never confuse "no data" with "still loading"
### 3. Loading States
For every async operation and data fetch:
- [ ] **Loading indicator exists**: User sees feedback that something is happening
- [ ] **Skeleton over spinner**: Use skeleton screens for content areas, spinners only for actions
- [ ] **No layout shift**: Content area maintains its dimensions during loading (prevents CLS)
- [ ] **Loading is fast-feeling**: Skeleton previews the content shape; perceived wait is minimised
- [ ] **Loading timeout**: If loading takes >5s, show a reassuring message ("This is taking longer than usual")
- [ ] **Button loading**: Buttons show inline loading state, remain disabled, and preserve their width
- [ ] **Optimistic updates**: For low-stakes actions, show success immediately and rollback on failure
### 4. Disabled States
For every interactive element that can be disabled:
- [ ] **Visually distinct**: Reduced opacity (0.38-0.5) or muted treatment — clearly non-interactive
- [ ] **`aria-disabled`**: Set alongside visual treatment for screen reader users
- [ ] **No pointer events**: `pointer-events: none` or equivalent — no hover/active states
- [ ] **Tooltip on disabled**: Explains WHY the element is disabled (e.g., "Complete the required fields first")
- [ ] **Cursor**: Shows `not-allowed` cursor on hover
### 5. Boundary Values
For every input that accepts user data:
- [ ] **Max length**: Text inputs have sensible `maxLength` and show remaining characters if relevant
- [ ] **Min/max values**: Number inputs have `min`/`max` attributes
- [ ] **Long content**: Component handles very long names, descriptions, and values without breaking layout
- [ ] **Short content**: Component handles single-character or minimal content gracefully
- [ ] **Special characters**: Handles ampersands, quotes, HTML entities, and emoji without rendering issues
- [ ] **Zero state**: Numeric displays handle $0.00, 0 items, 0 results
- [ ] **Large numbers**: Handles $999,999+ with proper formatting
### 6. Overflow & Truncation
For every text container and layout:
- [ ] **Text overflow**: Long text truncates with ellipsis or wraps gracefully — never overflows container
- [ ] **Responsive overflow**: No horizontal scroll at any viewport width
- [ ] **List overflow**: Long lists scroll within a container, not the page
- [ ] **Image overflow**: Images are constrained to their containers with `object-fit`
### 7. Keyboard & Focus
For every interactive element and flow:
- [ ] **Tab order**: Logical, matches visual order
- [ ] **Focus trap**: Modals and drawers trap focus correctly (use `inert` on background)
- [ ] **Focus return**: When a modal/popover closes, focus returns to the trigger element
- [ ] **Escape to close**: All overlays close on Escape key
- [ ] **Enter to submit**: Forms submit on Enter from the last field
- [ ] **Arrow navigation**: Tab lists, menus, and radio groups use roving tabindex with arrow keys
### 8. Concurrent & Async
For forms and flows with async operations:
- [ ] **Double-submit prevention**: Submit button disables after first click
- [ ] **Rapid interaction**: Debounced search, throttled scroll handlers
- [ ] **Stale data**: Component handles data that changes between render and interaction
- [ ] **Unmount safety**: Async operations clean up on component unmount (no state updates after unmount)
## Hardening Report
After fixing issues, provide a summary:
### Fixed
List each issue fixed with a one-line description.
### Verified OK
List categories that passed inspection without changes needed.
### Out of Scope
Note any structural issues found that require architectural changes (not hardening work).
**NEVER**:
- Use aggressive red for error states — always copper/warm tones per D024
- Show blank screens for empty or error states
- Leave async operations without loading feedback
- Allow double-submit on forms
- Remove focus indicators
- Make structural changes — hardening fixes edge cases within the existing architecture

View File

@@ -0,0 +1,146 @@
---
name: normalize
description: Cross-component consistency scan — checks token access patterns, transitions, focus styles, spacing methods, and displayName across all components in a tier or the entire system.
user-invocable: true
argument-hint: "[tier (atoms/molecules/organisms) or 'all']"
---
Scan all components in a tier (or the entire system) for consistency violations and fix them. Unlike audit/critique which evaluate individual components, **normalize ensures the system behaves as one cohesive whole**. This skill finds AND fixes issues.
**Target tier:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA design conventions
2. Read `docs/memory/decisions-log.md` for design rationale (especially D031 on token access)
3. Read `docs/conventions/component-conventions.md` for structural rules
4. List all component files in the target tier(s):
- Atoms: `src/components/atoms/*/`
- Molecules: `src/components/molecules/*/`
- Organisms: `src/components/organisms/*/`
- If target is "all", scan every tier
**FA context**: Consistency is trust. When components behave differently for no reason — different transition speeds, different focus styles, different spacing methods — users sense it even if they cannot articulate it. For families in distress, inconsistency creates subconscious unease. Normalize ruthlessly.
## Consistency Dimensions
For each dimension, scan ALL components in the target scope, compare patterns, identify outliers, and fix them to match the established convention.
### 1. Token Access Patterns (D031)
**Convention**: Two access methods, by token tier:
- **Semantic tokens** (colour, spacing, typography, shape): `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*` inside theme callbacks. `var(--fa-color-*)`, `var(--fa-spacing-*)` in static contexts.
- **Component tokens** (badge sizes, card shadows, input dimensions): `var(--fa-badge-*)`, `var(--fa-card-*)` CSS variables only — these are NOT in the MUI theme.
**Scan for violations**:
- [ ] Hardcoded hex/rgb colour values (should use theme or CSS var)
- [ ] Hardcoded px spacing values (should use `theme.spacing()` or `var(--fa-spacing-*)`)
- [ ] Hardcoded font sizes or weights (should use `theme.typography.*`)
- [ ] Semantic tokens accessed via CSS var when inside a theme callback (prefer theme accessor)
- [ ] Component tokens accessed via theme (they are CSS vars only)
- [ ] Primitive tokens used directly instead of semantic tokens
### 2. Transition Timing
**Convention**: 150ms ease-in-out for all state transitions.
**Scan for violations**:
- [ ] Transitions using durations other than 150ms (or `theme.transitions.duration.short`)
- [ ] Transitions using easing other than ease-in-out
- [ ] Transitions using bounce, elastic, or spring easing (remove — feels dated)
- [ ] Missing transitions on interactive state changes (hover, focus, active)
- [ ] Transitions on layout properties (`width`, `height`, `top`, `left`) instead of `transform`/`opacity`
### 3. Focus-Visible Style
**Convention**: `:focus-visible` with 2px outline, offset 2px, high contrast (3:1 minimum against adjacent colours).
**Scan for violations**:
- [ ] `outline: none` without `:focus-visible` replacement
- [ ] Focus styles on `:focus` instead of `:focus-visible` (shows ring on click)
- [ ] Inconsistent outline width, colour, or offset across components
- [ ] Missing focus styles entirely on interactive elements
- [ ] Focus ring colour that does not meet 3:1 contrast
### 4. Spacing Method
**Convention**: `theme.spacing()` in styled components and `sx` props. `var(--fa-spacing-*)` in static contexts.
**Scan for violations**:
- [ ] Raw pixel values for padding/margin/gap (e.g., `padding: '16px'`)
- [ ] Mixed methods in the same component (some `theme.spacing()`, some raw values)
- [ ] Inconsistent spacing scale usage across similar components (e.g., one card uses `spacing(2)` for padding, another uses `spacing(3)`)
### 5. Component Structure
**Convention**: Per `docs/conventions/component-conventions.md`.
**Scan for violations**:
- [ ] Missing `displayName` on the component
- [ ] Missing barrel export (`index.ts` with `export { default }` and `export *`)
- [ ] Missing JSDoc on props interface (every prop needs a `/** description */`)
- [ ] Missing JSDoc on the component itself
- [ ] Props defined as `type` instead of `interface` (interfaces produce better autodocs)
- [ ] Missing `sx` prop forwarding (every component must accept consumer overrides)
- [ ] Interactive elements not using `React.forwardRef`
### 6. Story Coverage
**Convention**: Per story coverage checklist in `docs/conventions/component-conventions.md`.
**Scan for violations**:
- [ ] Missing `tags: ['autodocs']` in story meta
- [ ] Missing Default story
- [ ] Missing AllVariants story (if component has variants)
- [ ] Missing Disabled story (if component can be disabled)
- [ ] Missing Loading story (if component has loading state)
- [ ] Incorrect `title` prefix (should match tier: `Atoms/`, `Molecules/`, `Organisms/`)
### 7. Naming Consistency
**Scan for violations**:
- [ ] Component folder not in PascalCase
- [ ] File names not matching component name
- [ ] Inconsistent prop naming across similar components (e.g., `isDisabled` vs `disabled`, `colour` vs `color`)
- [ ] CSS custom properties not prefixed with `--fa-`
## Normalize Process
1. **Scan**: Read every component file in the target tier(s)
2. **Tabulate**: Build a comparison table for each dimension showing what each component does
3. **Identify outliers**: Find components that deviate from the convention
4. **Fix**: Update outlier components to match the convention
5. **Verify**: Ensure TypeScript compiles and Storybook renders after fixes
## Normalization Report
Present findings in this format:
### Scan Summary
| Dimension | Components scanned | Violations found | Fixed |
|-----------|-------------------|-----------------|-------|
| Token access | ? | ? | ? |
| Transitions | ? | ? | ? |
| Focus styles | ? | ? | ? |
| Spacing | ? | ? | ? |
| Structure | ? | ? | ? |
| Stories | ? | ? | ? |
| Naming | ? | ? | ? |
### Violations Fixed
For each fix, note:
- **Component**: Which component was changed
- **Dimension**: Which consistency rule was violated
- **Before**: What it was doing
- **After**: What it does now
### System-Level Observations
Note any patterns that suggest a convention should be updated (e.g., if most components deviate from the convention, perhaps the convention is wrong).
**NEVER**:
- Change a component's behaviour or API — normalize only changes implementation details
- Fix one component and leave similar components unfixed — normalize the whole tier
- Change conventions without flagging it — if you think a convention should change, note it as an observation, do not unilaterally change the rule

View File

@@ -0,0 +1,146 @@
---
name: polish
description: Final production readiness pass — visual alignment, spacing consistency, interaction states, copy, edge cases. Use before marking a component done.
user-invocable: true
argument-hint: "[component or area to polish]"
---
Perform a meticulous final pass on a component or area, fixing every detail that separates good work from great work. Unlike audit/critique which only assess, **polish finds AND fixes issues**.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA design conventions
2. Read `docs/memory/decisions-log.md` for design rationale and prior decisions
3. Read the target component/area source files and stories
4. Reference `docs/reference/impeccable/polish-skill.md` for the full checklist
5. Reference `docs/conventions/component-conventions.md` for structural rules
**FA context**: Funeral Arranger serves families in grief or distress. The design must feel warm, trustworthy, and calm. Touch targets >= 44px. Transitions 150ms ease-in-out. No aggressive colours for errors (copper, not red — see D024). Accessibility is critical — users may be elderly, emotional, or on mobile.
**CRITICAL**: Polish is the last step, not the first. Do not polish work that is not functionally complete. If the component has open TODOs or missing features, flag them and skip those areas.
## Pre-Polish Assessment
Before fixing anything, assess the current state:
1. **Review completeness** — Is the component functionally complete? Are there known issues to preserve (mark with TODOs)?
2. **Identify polish areas** — Scan for visual inconsistencies, spacing issues, interaction state gaps, copy problems, edge cases, and transition roughness.
3. **Set scope** — List the specific fixes to make. Do not attempt structural changes — polish is micro-detail work.
## Polish Systematically
Work through each dimension. **Fix issues as you find them** — do not just document.
### 1. Visual Alignment & Spacing
- **Grid alignment**: All elements line up to the spacing scale (no arbitrary gaps)
- **Consistent spacing**: Every gap uses `theme.spacing()` or `var(--fa-spacing-*)` — no magic numbers
- **Optical alignment**: Adjust for visual weight where needed (icons often need offset for optical centering)
- **Responsive consistency**: Spacing works at all breakpoints (mobile, tablet, desktop)
**How to check**: Inspect computed styles. Look for values that are not multiples of 4px or 8px. Squint at the layout — anything that feels off probably is.
### 2. Typography Refinement
- **Hierarchy consistency**: Same element types use the same typography variant throughout
- **Line length**: Body text stays within 45-75 characters
- **Line height**: Appropriate for font size and reading context
- **Token compliance**: All font sizes, weights, and families come from `theme.typography.*`
### 3. Colour & Contrast
- **Contrast ratios**: All text meets WCAG AA (4.5:1 for normal text, 3:1 for large text and UI)
- **Token usage**: No hardcoded hex values — all via theme palette or CSS variables
- **Tinted neutrals**: No pure gray or pure black — add subtle colour tint per FA palette
- **Gray on colour**: Never put gray text on coloured backgrounds — use a shade of that colour
### 4. Interaction States
Every interactive element must have ALL of these states:
| State | Treatment | FA note |
|-------|-----------|---------|
| Default | Base styling | Warm, inviting |
| Hover | Subtle lift, colour shift | 150ms ease-in-out |
| Focus | Visible ring via `:focus-visible` | 2px, offset 2px, high contrast |
| Active | Pressed/darker | Immediate feedback |
| Disabled | Reduced opacity, `aria-disabled` | Clearly non-interactive |
| Loading | Spinner or skeleton | Reduce perceived wait |
| Error | Copper border/text, not red | Gentle, per D024 |
| Success | Confirmation feedback | Reassuring, not flashy |
**The common miss**: Designing hover without focus. Keyboard users never see hover states.
### 5. Micro-interactions & Transitions
- **Timing**: All transitions 150ms ease-in-out (FA convention)
- **Properties**: Only animate `transform`, `opacity`, `background-color`, `border-color`, `box-shadow` — never animate `width`, `height`, or `top`/`left`
- **Reduced motion**: Must respect `prefers-reduced-motion`
- **Easing**: Ease-in-out or ease-out. Never bounce or elastic — they feel dated.
### 6. Copy & Content
- **Consistent terminology**: Same things called the same names throughout
- **Capitalisation**: Sentence case for body, consistent for labels
- **Tone**: Warm, professional, clear — no jargon, no condescension
- **Labels**: Unambiguous — a user should never wonder "what does this mean?"
- **Punctuation**: Consistent (periods on sentences, not on labels)
### 7. Edge Cases
- **Long content**: Handles very long names, descriptions, prices
- **Empty states**: Helpful guidance, not blank space
- **Missing data**: Graceful degradation with sensible defaults
- **Loading states**: Clear async feedback, skeleton over spinner where possible
- **Error states**: Helpful, non-blaming messages with recovery paths (copper, not red)
### 8. Code Cleanliness
- Remove `console.log` statements
- Remove commented-out code
- Remove unused imports
- Verify `displayName` is set on the component
- Verify barrel export in `index.ts`
- Ensure all props have JSDoc descriptions
- No TypeScript `any` types
## Polish Checklist
Verify each item after completing fixes:
- [ ] Visual alignment correct at all breakpoints
- [ ] Spacing uses design tokens consistently (no magic numbers)
- [ ] Typography hierarchy consistent
- [ ] All interactive states implemented (hover, focus, active, disabled, loading, error)
- [ ] All transitions 150ms ease-in-out
- [ ] Focus indicators visible via `:focus-visible`
- [ ] Copy is consistent, warm, and unambiguous
- [ ] Touch targets >= 44px on all interactive elements
- [ ] Contrast ratios meet WCAG AA
- [ ] Keyboard navigation works correctly
- [ ] No console errors or warnings
- [ ] No layout shift on load
- [ ] Respects `prefers-reduced-motion`
- [ ] Code is clean (no TODOs, console.logs, commented code, unused imports)
- [ ] Component has `displayName` and barrel export
- [ ] All props have JSDoc descriptions
## Final Verification
After all fixes:
1. **Check Storybook** — Verify all stories render correctly
2. **Check TypeScript** — Ensure no type errors introduced
3. **Compare states** — Walk through every interactive state visually
4. **Test keyboard** — Tab through the component, verify focus order and indicators
**NEVER**:
- Polish before the component is functionally complete
- Introduce bugs while polishing (test thoroughly after each fix)
- Ignore systematic issues (if spacing is off everywhere, fix the system not individual instances)
- Perfect one area while leaving others rough (maintain consistent quality level)
- Make structural or architectural changes — that is not polish work
Report a summary of what was fixed when done.

View File

@@ -1,6 +1,6 @@
---
name: preflight
description: Pre-commit quality check — verifies TypeScript, Storybook, token sync, and no hardcoded values
description: Pre-commit quality check — verifies TypeScript, ESLint, Prettier, Storybook, token sync, and no hardcoded values
argument-hint: "[--fix to auto-fix issues]"
---
@@ -62,6 +62,24 @@ find src/components/atoms/ -name "*.tsx" ! -name "*.stories.tsx" | xargs grep -L
- **Fail:** Missing exports or displayName
- **Critical:** No — warn but don't block commit
### 6. ESLint
```bash
npm run lint 2>&1
```
- **Pass:** No errors
- **Fail:** ESLint errors → report them
- **Fix:** Run `npm run lint:fix`
- **Critical:** Yes — do not commit if this fails
### 7. Prettier
```bash
npm run format:check 2>&1
```
- **Pass:** All files formatted correctly
- **Fail:** Formatting issues found → report them
- **Fix:** Run `npm run format`
- **Critical:** No — warn but don't block commit (Husky pre-commit hook auto-fixes these)
### Report format
```
PREFLIGHT RESULTS
@@ -71,6 +89,8 @@ PREFLIGHT RESULTS
✓ Token sync ............. PASS
⚠ Hardcoded values ....... WARN (2 issues)
✓ Component exports ...... PASS
✓ ESLint ................. PASS
✓ Prettier ............... PASS
───────────────────────────────
Result: PASS (safe to commit)
```
@@ -79,7 +99,7 @@ Use `PASS`, `FAIL`, or `WARN`. If any critical check fails, the result is `FAIL
If `--fix` was passed, attempt to fix issues automatically (e.g., run `npm run build:tokens` for stale tokens) and re-check.
### 6. Visual QA spot-check (manual review, non-blocking)
### 8. Visual QA spot-check (manual review, non-blocking)
If a component was recently modified, do a quick visual review of the source code for these common issues (adapted from impeccable /polish):

View File

@@ -0,0 +1,131 @@
---
name: quieter
description: Calmer design pass — reduces visual noise, aggressive colours, competing weights, and transactional copy. Highly FA-specific for grief-sensitive context.
user-invocable: true
argument-hint: "[component or area to quiet]"
---
Run a grief-sensitive design pass on a component or area, then **fix issues found**. This is assessment AND fix — diagnose anything that feels too loud, too urgent, or too transactional for a funeral planning context, then apply corrections.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA brand guidelines and tone
2. Read `docs/memory/decisions-log.md` for colour and tone decisions (especially D024 for error treatment)
3. Read the target component/area source files and stories
4. Reference `docs/reference/impeccable/color-and-contrast.md` for colour usage principles
5. Reference `docs/reference/impeccable/personas.md` for user archetypes (especially Jordan — confused first-timer, and Casey — distracted mobile user)
**FA context reminder**: "Understated empathy — warm but not gushy. Let UI structure be the empathy." Users are often elderly, emotional, and unfamiliar with technology. They may be planning a funeral for a loved one who has just died. Design must feel like a trusted advisor, not a sales platform. Every element that demands attention should earn it. Silence and space are design tools.
## Diagnostic Checks
Work through each check systematically. For each issue found, note the problem, then fix it.
### 1. Aggressive Colour Treatment
- **Error states must NOT use aggressive red** — FA uses a softer copper/warm treatment for errors (per D024)
- Check for bright, saturated reds (#ff0000, #e53935, MUI error.main defaults) in error messages, validation, or alerts
- Replace with the FA copper error palette from the theme (`palette.error.main` should already be the soft treatment)
- Warning states should also be warm, not harsh yellow/orange
- Success states should be muted green, not vivid/celebratory
- Check that semantic colours come from the theme, not hardcoded aggressive values
### 2. High Saturation Where Muted Would Be Better
- Scan for highly saturated colours that draw disproportionate attention
- Background colours should be muted — surfaces use the warm neutral palette, not bright fills
- Accent colours work because they are rare (60-30-10 rule) — if accent is used liberally, it stops working
- Icons and decorative elements should use muted tones unless they serve a functional purpose
- Check that the warm gold/copper brand palette is cohesive — no jarring colour outliers
### 3. Competing Visual Weights
- Look for multiple bold or heavy elements fighting for attention in the same view
- There should be one clear primary focal point — if multiple elements scream for attention, quiet the secondary ones
- Bold text should be used for emphasis, not as default — if everything is bold, nothing is
- Check for multiple large/heavy headings at the same level competing with each other
- Reduce weight on supporting elements (metadata, labels, secondary info) to let primary content breathe
### 4. Insufficient Whitespace
- Check padding and margins — grief-sensitive design needs generous breathing room
- Cards, sections, and content blocks should have ample internal padding
- Vertical spacing between sections should feel restful, not cramped
- Dense layouts feel overwhelming for users in emotional distress — space creates calm
- Check that spacing values come from the theme spacing scale, and lean toward the generous end
### 5. Urgent Animations and Transitions
- Animations should feel gentle, not snappy or urgent
- Check for bounce/elastic easing — replace with ease-out or ease-in-out
- Transition durations should be moderate (200-400ms for UI, longer for content reveals)
- No attention-grabbing animations (pulse, shake, flash) — these feel demanding
- Loading states should feel patient, not frantic
- If an animation can be removed without losing meaning, consider removing it
### 6. Transactional Copy
- Copy should feel empathetic and guiding, not sales-driven or transactional
- Check button labels: "Buy Now", "Add to Cart", "Submit" feel transactional — prefer "Continue", "Next Step", "Confirm Details"
- Check headings: avoid language that feels like a sales funnel ("Choose Your Package", "Upgrade", "Best Value")
- Error messages should be gentle and helpful, never blaming ("Something went wrong" not "You entered an invalid...")
- Avoid urgency language ("Limited time", "Act now", "Don't miss out") — this is deeply inappropriate in a grief context
- Pricing should be presented transparently, not with sales psychology (no crossed-out prices, no "savings" badges)
- Check for placeholder/lorem text that may have a casual or inappropriate tone
### 7. CTA Hierarchy
- There should be ONE clear primary CTA per view/section — not multiple competing calls to action
- Secondary actions should be visually distinct (outlined, text-only) not just smaller versions of primary buttons
- If there are more than 2 CTAs visible simultaneously, evaluate whether some can be demoted or removed
- The primary CTA should feel inviting, not pressuring — warm brand colour, not aggressive contrast
- Check that button hierarchy uses theme variants correctly (contained for primary, outlined for secondary, text for tertiary)
## Fix Process
For each issue found:
1. **Identify** the file, line, and current value
2. **Assess emotional impact** — how does this element feel to a grieving user?
3. **Determine the quieter alternative** — what change reduces noise while preserving function?
4. **Apply the fix** — update colours, weights, spacing, copy, or animation values
5. **Verify** the fix doesn't make the element invisible or non-functional — quiet does not mean hidden
## Report Format
After fixing, present a summary:
### Quiet Pass Summary
| Check | Status | Issues Found | Issues Fixed |
|-------|--------|-------------|-------------|
| Aggressive Colours | pass/warn/fail | N | N |
| High Saturation | pass/warn/fail | N | N |
| Competing Weights | pass/warn/fail | N | N |
| Whitespace | pass/warn/fail | N | N |
| Animations/Transitions | pass/warn/fail | N | N |
| Transactional Copy | pass/warn/fail | N | N |
| CTA Hierarchy | pass/warn/fail | N | N |
### Emotional Tone Assessment
Brief statement on the overall emotional register of the component:
- Does it feel like a **trusted advisor** or a **sales platform**?
- Would a grieving family member feel **supported** or **pressured**?
- Is the design **calm** or **demanding**?
### Changes Made
For each fix applied:
- **What changed**: Brief description
- **File**: Path and relevant line(s)
- **Before/After**: The old and new values
- **Why**: How this change serves the grief-sensitive context
### Remaining Concerns
Any issues that need design input, copy review, or are outside the scope of a quieting pass.
**NEVER**: Use aggressive red for error states. Add urgency language or sales psychology. Make everything equally loud. Remove functional affordances in the name of minimalism. Ignore that real people in real pain will use this interface.

View File

@@ -13,6 +13,7 @@ Review a component against FA Design System conventions and report pass/fail for
2. Read `docs/conventions/token-conventions.md` for token usage rules
3. Read the component source file in `src/components/`
4. Read the component's Storybook stories
5. Reference `docs/reference/vercel/react-best-practices.md` for React performance patterns (optional, for deeper code quality review)
**Check each of these and report pass/fail:**

View File

@@ -0,0 +1,116 @@
---
name: typeset
description: Typography refinement — checks hierarchy consistency, line length, line height, font weight, and mobile readability across a component or area.
user-invocable: true
argument-hint: "[component or area to typeset]"
---
Run a typography-focused review on a component or area, then **fix issues found**. This is assessment AND fix — diagnose problems, then apply corrections.
**Target:** $ARGUMENTS
## Preparation
1. Read `docs/design-system.md` for FA typography conventions
2. Read `docs/memory/token-registry.md` for the current type scale and weight decisions
3. Read `docs/memory/decisions-log.md` for typography-related decisions (especially D017, D019)
4. Read the target component/area source files and stories
5. Reference `docs/reference/impeccable/typography.md` for detailed typographic guidelines
**FA context reminder**: Funeral Arranger serves families often in grief or distress. Many users are elderly, on mobile, and reading under emotional strain. Typography must prioritise readability and calm hierarchy over visual flair. Warmth comes through the serif display type (Noto Serif SC) at display sizes — not through decorative overuse. Body text must be effortlessly readable.
## Diagnostic Checks
Work through each check systematically. For each issue found, note the problem, then fix it.
### 1. Hierarchy Consistency
- Same semantic level must have the same visual treatment everywhere in the target
- Heading levels should use the correct MUI typography variant (h1-h6, subtitle1/2, body1/2)
- No more than 3-4 distinct text sizes visible at once (muddy hierarchy = too many similar sizes)
- Size jumps between levels should be clear — avoid sizes too close together (e.g., 14px, 15px, 16px)
- Check that the modular scale from the token system is followed consistently
### 2. Line Length (Measure)
- Body text should be constrained to 45-75 characters per line (`max-width: 65ch` is a good default)
- If body text runs full-width on desktop, add a `max-width` using `ch` units
- Narrow columns (sidebars, cards) may go below 45ch — tighten line-height to compensate
- Wide text blocks strain reading comprehension, especially for elderly/distressed users
### 3. Line Height
- Body text: 1.5-1.6 line-height for comfortable reading
- Headings: tighter line-height (1.1-1.3) since larger text needs less relative leading
- Line-height scales inversely with line length — narrow columns need tighter leading, wide columns need more
- Light text on dark backgrounds needs ~0.05-0.1 more line-height than normal
- Vertical rhythm: spacing values should relate to the base line-height unit
### 4. Font Weight Consistency
- **Body weight must be Medium 500** (per D019) — check that body text is not using 400 (Regular)
- Headings should use consistent weights within tiers (all h2s same weight, all h3s same weight)
- Avoid more than 3 font weights in the same view — too many competing weights create noise
- Bold should be used for emphasis, not as a default state for UI elements
- Check that weight values come from the theme, not hardcoded
### 5. Display Font Usage
- **Noto Serif SC is for display variants only** (per D017) — hero text, display headings, featured quotes
- Noto Serif SC must NOT be used for standard headings (h1-h6 in regular UI contexts)
- Standard headings use the sans-serif stack (Figtree or system font)
- If the serif font appears on a regular heading, replace it with the sans-serif variant
### 6. Mobile Font Size Adequacy
- Body text minimum 14px on mobile (16px preferred, per impeccable guidelines)
- Small/caption text minimum 12px
- Touch-target text (buttons, links in body) must be large enough to tap accurately
- Check that `rem` units are used (not `px`) so text respects browser zoom settings
- Verify no `user-scalable=no` in viewport meta (breaks accessibility)
### 7. Token Compliance
- All font sizes should come from theme.typography variants, not hardcoded values
- All font weights should come from theme tokens
- All line-heights should come from theme tokens
- Font family should come from theme, never hardcoded strings
- Check for inline `style` props that override typography tokens
## Fix Process
For each issue found:
1. **Identify** the file, line, and current value
2. **Reference** the correct token/theme value it should use
3. **Apply the fix** — update the component source to use the correct theme value
4. **Verify** the fix maintains visual intent (don't blindly replace — ensure the result looks right)
## Report Format
After fixing, present a summary:
### Typography Health Summary
| Check | Status | Issues Found | Issues Fixed |
|-------|--------|-------------|-------------|
| Hierarchy Consistency | pass/warn/fail | N | N |
| Line Length | pass/warn/fail | N | N |
| Line Height | pass/warn/fail | N | N |
| Font Weight | pass/warn/fail | N | N |
| Display Font Usage | pass/warn/fail | N | N |
| Mobile Font Size | pass/warn/fail | N | N |
| Token Compliance | pass/warn/fail | N | N |
### Changes Made
For each fix applied:
- **What changed**: Brief description
- **File**: Path and relevant line(s)
- **Before/After**: The old and new values
### Remaining Concerns
Any issues that need design input or are outside the scope of a typesetting pass.
**NEVER**: Hardcode font sizes in px. Use the serif display font (Noto Serif SC) for regular headings. Set body weight to 400 (Regular) — FA uses 500 (Medium). Remove typography tokens in favour of inline styles. Ignore mobile readability.

View File

@@ -0,0 +1,101 @@
---
name: write-tests
description: Write or update tests for a component — determines whether it needs unit tests (Vitest), interaction tests (Storybook play), or both, then generates appropriate test code.
user-invocable: true
argument-hint: "[ComponentName]"
---
Write tests for the specified component.
**Component:** $ARGUMENTS
## Preparation
1. Read `docs/conventions/component-conventions.md` for component patterns
2. Read the component source file in `src/components/`
3. Read the component's existing Storybook stories
4. Check `docs/memory/component-registry.md` for component status and composition
## Determine Test Strategy
Categorise the component:
### Interactive components (need Storybook `play` functions)
Components with user interactions: clicks, toggles, keyboard navigation, form inputs, selection state changes.
**Examples:** Button, Input, SearchBar, ServiceOption, AddOnOption, Switch, Radio, FuneralFinder
For these, add `play` functions to existing stories:
```tsx
import { expect, userEvent, within } from '@storybook/test';
export const ClickTest: Story = {
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button = canvas.getByRole('button');
await userEvent.click(button);
await expect(button).toBeVisible();
},
};
```
**What to test in `play` functions:**
- Click/tap fires expected callback
- Disabled state prevents interaction
- Keyboard navigation works (Enter, Space, Arrow keys)
- Loading state disables interaction
- Error states show correct feedback
- Selection state changes visually
- Form validation triggers on submit
### Logic-heavy components (need Vitest unit tests)
Components with significant internal logic: conditional rendering, validation, state machines, computed values.
**Examples:** FuneralFinder (validation logic), PackageDetail (price calculations), ServiceSelector (selection management)
Create `{ComponentName}.test.tsx` alongside the component:
```tsx
import { describe, it, expect } from 'vitest';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider } from '@mui/material/styles';
import theme from '../../../theme';
import { ComponentName } from './ComponentName';
const renderWithTheme = (ui: React.ReactElement) =>
render(<ThemeProvider theme={theme}>{ui}</ThemeProvider>);
describe('ComponentName', () => {
it('renders with default props', () => {
renderWithTheme(<ComponentName />);
expect(screen.getByRole('...')).toBeInTheDocument();
});
});
```
**What to test in Vitest:**
- Conditional rendering logic (shows/hides elements based on props)
- Validation rules (required fields, format checks)
- Callback props fire with correct arguments
- Accessibility: correct ARIA roles and states
- Edge cases: empty data, maximum values, missing optional props
### Display-only components (minimal testing needed)
Components that only render static content from props: Typography, Badge, Divider, Card (non-interactive).
For these, stories ARE the tests. Ensure stories cover all variants. No additional test files needed unless there's conditional rendering logic.
## After Writing Tests
1. Run `npm run test` to verify Vitest tests pass
2. If Storybook `play` functions were added, verify they work in Storybook's test panel
3. Update `docs/memory/component-registry.md` with test status note
## Rules
- Always wrap components in `ThemeProvider` with FA theme in Vitest tests
- Use `screen.getByRole()` over `getByTestId()` — test what the user sees
- Test behaviour, not implementation — don't test internal state directly
- Keep tests focused: one assertion per test where practical
- Don't test MUI internals — only test our component's API
- Don't snapshot test — snapshots are too brittle for an evolving design system

1
.husky/pre-commit Normal file
View File

@@ -0,0 +1 @@
npx lint-staged

4
.prettierignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
storybook-static/
src/theme/generated/
tokens/export/

10
.prettierrc Normal file
View File

@@ -0,0 +1,10 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"tabWidth": 2,
"semi": true,
"arrowParens": "always",
"bracketSpacing": true,
"endOfLine": "lf"
}

View File

@@ -73,7 +73,7 @@ This project uses structured markdown files for cross-session memory.
- `docs/memory/decisions-log.md` — All design decisions with rationale
- `docs/memory/component-registry.md` — Status of every component (planned/in-progress/done)
- `docs/memory/token-registry.md` — All tokens with their current values and usage notes
- `docs/memory/session-log.md`What was done in previous sessions, what's next
- `docs/memory/session-log.md`Recent sessions (last 2-3); older sessions in `docs/memory/archive/`
**After completing work, update:**
- The relevant memory files with what changed

View File

@@ -95,7 +95,9 @@ export default Button;
### Theming
- **NEVER hardcode** colour values, spacing, font sizes, shadows, or border radii
- Use `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*`
- **Semantic tokens** (text, surface, border, interactive, feedback, typography, spacing): use MUI theme accessors — `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*` — when inside a theme callback. CSS variables (`var(--fa-color-*)`, `var(--fa-spacing-*)`) are also acceptable for semantic tokens when more ergonomic (e.g., static colour maps, non-callback contexts).
- **Component tokens** (badge sizes, card shadows, input dimensions, etc.): use CSS variables (`var(--fa-badge-*)`, `var(--fa-card-*)`) — these are NOT mapped into the MUI theme.
- See decision D031 in `docs/memory/decisions-log.md` for full rationale.
- For one-off theme overrides, use the `sx` prop pattern
- Every component MUST accept and forward the `sx` prop for consumer overrides

File diff suppressed because it is too large Load Diff

View File

@@ -259,3 +259,13 @@ contradict a previous one.
**Rationale:** A provider could be verified but have a broken/missing image URL. The boolean is the source of truth from the business layer. Image and logo are only rendered when `verified` is also true, preventing accidental display of unverified providers with images.
**Affects:** ProviderCard API, data model expectations
**Alternatives considered:** Deriving verified from imageUrl presence — rejected as it couples business logic (partner status) to content availability (image uploaded).
### D031 — Token access convention: theme accessors for semantic, CSS vars for component
**Date:** 2026-03-27
**Category:** architecture
**Decision:** Two token access methods are both valid, used by tier:
- **`theme.palette.*`, `theme.typography.*`, `theme.spacing()`** — for semantic tokens that MUI maps into its theme (palette colours, typography variants, spacing). Used when the MUI theme callback is already available.
- **`var(--fa-*)`** CSS variables — for component-tier tokens (badge sizes, card shadows, input dimensions) generated by Style Dictionary but not mapped into the MUI theme. Also acceptable for semantic tokens when the CSS variable form is more ergonomic (e.g., in colour maps, static styles).
**Rationale:** The codebase naturally evolved this dual-path pattern. Semantic tokens are mapped into the MUI theme by `src/theme/index.ts`, making `theme.palette.*` the idiomatic MUI access path. Component-tier tokens exist only as CSS variables (Style Dictionary output). Forcing all access through one path would either require mapping every component token into MUI (over-engineering) or abandoning MUI's type-safe theme accessors (losing DX).
**Affects:** All component files, `docs/conventions/component-conventions.md`, Badge.tsx (normalized to CSS vars for colour maps)
**Alternatives considered:** CSS vars only — rejected because it loses MUI's TypeScript-safe theme API. Theme accessors only — rejected because component tokens aren't in the MUI theme.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,145 @@
---
name: vercel-react-best-practices
description: React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
license: MIT
metadata:
author: vercel
version: "1.0.0"
---
# Vercel React Best Practices
Comprehensive performance optimization guide for React and Next.js applications, maintained by Vercel. Contains 65 rules across 8 categories, prioritized by impact to guide automated refactoring and code generation.
## When to Apply
Reference these guidelines when:
- Writing new React components or Next.js pages
- Implementing data fetching (client or server-side)
- Reviewing code for performance issues
- Refactoring existing React/Next.js code
- Optimizing bundle size or load times
## Rule Categories by Priority
| Priority | Category | Impact | Prefix |
|----------|----------|--------|--------|
| 1 | Eliminating Waterfalls | CRITICAL | `async-` |
| 2 | Bundle Size Optimization | CRITICAL | `bundle-` |
| 3 | Server-Side Performance | HIGH | `server-` |
| 4 | Client-Side Data Fetching | MEDIUM-HIGH | `client-` |
| 5 | Re-render Optimization | MEDIUM | `rerender-` |
| 6 | Rendering Performance | MEDIUM | `rendering-` |
| 7 | JavaScript Performance | LOW-MEDIUM | `js-` |
| 8 | Advanced Patterns | LOW | `advanced-` |
## Quick Reference
### 1. Eliminating Waterfalls (CRITICAL)
- `async-defer-await` - Move await into branches where actually used
- `async-parallel` - Use Promise.all() for independent operations
- `async-dependencies` - Use better-all for partial dependencies
- `async-api-routes` - Start promises early, await late in API routes
- `async-suspense-boundaries` - Use Suspense to stream content
### 2. Bundle Size Optimization (CRITICAL)
- `bundle-barrel-imports` - Import directly, avoid barrel files
- `bundle-dynamic-imports` - Use next/dynamic for heavy components
- `bundle-defer-third-party` - Load analytics/logging after hydration
- `bundle-conditional` - Load modules only when feature is activated
- `bundle-preload` - Preload on hover/focus for perceived speed
### 3. Server-Side Performance (HIGH)
- `server-auth-actions` - Authenticate server actions like API routes
- `server-cache-react` - Use React.cache() for per-request deduplication
- `server-cache-lru` - Use LRU cache for cross-request caching
- `server-dedup-props` - Avoid duplicate serialization in RSC props
- `server-hoist-static-io` - Hoist static I/O (fonts, logos) to module level
- `server-serialization` - Minimize data passed to client components
- `server-parallel-fetching` - Restructure components to parallelize fetches
- `server-parallel-nested-fetching` - Chain nested fetches per item in Promise.all
- `server-after-nonblocking` - Use after() for non-blocking operations
### 4. Client-Side Data Fetching (MEDIUM-HIGH)
- `client-swr-dedup` - Use SWR for automatic request deduplication
- `client-event-listeners` - Deduplicate global event listeners
- `client-passive-event-listeners` - Use passive listeners for scroll
- `client-localstorage-schema` - Version and minimize localStorage data
### 5. Re-render Optimization (MEDIUM)
- `rerender-defer-reads` - Don't subscribe to state only used in callbacks
- `rerender-memo` - Extract expensive work into memoized components
- `rerender-memo-with-default-value` - Hoist default non-primitive props
- `rerender-dependencies` - Use primitive dependencies in effects
- `rerender-derived-state` - Subscribe to derived booleans, not raw values
- `rerender-derived-state-no-effect` - Derive state during render, not effects
- `rerender-functional-setstate` - Use functional setState for stable callbacks
- `rerender-lazy-state-init` - Pass function to useState for expensive values
- `rerender-simple-expression-in-memo` - Avoid memo for simple primitives
- `rerender-split-combined-hooks` - Split hooks with independent dependencies
- `rerender-move-effect-to-event` - Put interaction logic in event handlers
- `rerender-transitions` - Use startTransition for non-urgent updates
- `rerender-use-deferred-value` - Defer expensive renders to keep input responsive
- `rerender-use-ref-transient-values` - Use refs for transient frequent values
- `rerender-no-inline-components` - Don't define components inside components
### 6. Rendering Performance (MEDIUM)
- `rendering-animate-svg-wrapper` - Animate div wrapper, not SVG element
- `rendering-content-visibility` - Use content-visibility for long lists
- `rendering-hoist-jsx` - Extract static JSX outside components
- `rendering-svg-precision` - Reduce SVG coordinate precision
- `rendering-hydration-no-flicker` - Use inline script for client-only data
- `rendering-hydration-suppress-warning` - Suppress expected mismatches
- `rendering-activity` - Use Activity component for show/hide
- `rendering-conditional-render` - Use ternary, not && for conditionals
- `rendering-usetransition-loading` - Prefer useTransition for loading state
- `rendering-resource-hints` - Use React DOM resource hints for preloading
- `rendering-script-defer-async` - Use defer or async on script tags
### 7. JavaScript Performance (LOW-MEDIUM)
- `js-batch-dom-css` - Group CSS changes via classes or cssText
- `js-index-maps` - Build Map for repeated lookups
- `js-cache-property-access` - Cache object properties in loops
- `js-cache-function-results` - Cache function results in module-level Map
- `js-cache-storage` - Cache localStorage/sessionStorage reads
- `js-combine-iterations` - Combine multiple filter/map into one loop
- `js-length-check-first` - Check array length before expensive comparison
- `js-early-exit` - Return early from functions
- `js-hoist-regexp` - Hoist RegExp creation outside loops
- `js-min-max-loop` - Use loop for min/max instead of sort
- `js-set-map-lookups` - Use Set/Map for O(1) lookups
- `js-tosorted-immutable` - Use toSorted() for immutability
- `js-flatmap-filter` - Use flatMap to map and filter in one pass
- `js-request-idle-callback` - Defer non-critical work to browser idle time
### 8. Advanced Patterns (LOW)
- `advanced-event-handler-refs` - Store event handlers in refs
- `advanced-init-once` - Initialize app once per app load
- `advanced-use-latest` - useLatest for stable callback refs
## How to Use
Read individual rule files for detailed explanations and code examples:
```
rules/async-parallel.md
rules/bundle-barrel-imports.md
```
Each rule file contains:
- Brief explanation of why it matters
- Incorrect code example with explanation
- Correct code example with explanation
- Additional context and references
## Full Compiled Document
For the complete guide with all rules expanded: `AGENTS.md`

View File

@@ -0,0 +1,39 @@
---
name: web-design-guidelines
description: Review UI code for Web Interface Guidelines compliance. Use when asked to "review my UI", "check accessibility", "audit design", "review UX", or "check my site against best practices".
metadata:
author: vercel
version: "1.0.0"
argument-hint: <file-or-pattern>
---
# Web Interface Guidelines
Review files for compliance with Web Interface Guidelines.
## How It Works
1. Fetch the latest guidelines from the source URL below
2. Read the specified files (or prompt user for files/pattern)
3. Check against all rules in the fetched guidelines
4. Output findings in the terse `file:line` format
## Guidelines Source
Fetch fresh guidelines before each review:
```
https://raw.githubusercontent.com/vercel-labs/web-interface-guidelines/main/command.md
```
Use WebFetch to retrieve the latest rules. The fetched content contains all the rules and output format instructions.
## Usage
When a user provides a file or pattern argument:
1. Fetch guidelines from the source URL above
2. Read the specified files
3. Apply all rules from the fetched guidelines
4. Output findings using the format specified in the guidelines
If no files specified, ask the user which files to review.

View File

@@ -0,0 +1,179 @@
---
description: Review UI code for Vercel Web Interface Guidelines compliance
argument-hint: <file-or-pattern>
---
# Web Interface Guidelines
Review these files for compliance: $ARGUMENTS
Read files, check against rules below. Output concise but comprehensive—sacrifice grammar for brevity. High signal-to-noise.
## Rules
### Accessibility
- Icon-only buttons need `aria-label`
- Form controls need `<label>` or `aria-label`
- Interactive elements need keyboard handlers (`onKeyDown`/`onKeyUp`)
- `<button>` for actions, `<a>`/`<Link>` for navigation (not `<div onClick>`)
- Images need `alt` (or `alt=""` if decorative)
- Decorative icons need `aria-hidden="true"`
- Async updates (toasts, validation) need `aria-live="polite"`
- Use semantic HTML (`<button>`, `<a>`, `<label>`, `<table>`) before ARIA
- Headings hierarchical `<h1>``<h6>`; include skip link for main content
- `scroll-margin-top` on heading anchors
### Focus States
- Interactive elements need visible focus: `focus-visible:ring-*` or equivalent
- Never `outline-none` / `outline: none` without focus replacement
- Use `:focus-visible` over `:focus` (avoid focus ring on click)
- Group focus with `:focus-within` for compound controls
### Forms
- Inputs need `autocomplete` and meaningful `name`
- Use correct `type` (`email`, `tel`, `url`, `number`) and `inputmode`
- Never block paste (`onPaste` + `preventDefault`)
- Labels clickable (`htmlFor` or wrapping control)
- Disable spellcheck on emails, codes, usernames (`spellCheck={false}`)
- Checkboxes/radios: label + control share single hit target (no dead zones)
- Submit button stays enabled until request starts; spinner during request
- Errors inline next to fields; focus first error on submit
- Placeholders end with `…` and show example pattern
- `autocomplete="off"` on non-auth fields to avoid password manager triggers
- Warn before navigation with unsaved changes (`beforeunload` or router guard)
### Animation
- Honor `prefers-reduced-motion` (provide reduced variant or disable)
- Animate `transform`/`opacity` only (compositor-friendly)
- Never `transition: all`—list properties explicitly
- Set correct `transform-origin`
- SVG: transforms on `<g>` wrapper with `transform-box: fill-box; transform-origin: center`
- Animations interruptible—respond to user input mid-animation
### Typography
- `…` not `...`
- Curly quotes `"` `"` not straight `"`
- Non-breaking spaces: `10&nbsp;MB`, `⌘&nbsp;K`, brand names
- Loading states end with `…`: `"Loading…"`, `"Saving…"`
- `font-variant-numeric: tabular-nums` for number columns/comparisons
- Use `text-wrap: balance` or `text-pretty` on headings (prevents widows)
### Content Handling
- Text containers handle long content: `truncate`, `line-clamp-*`, or `break-words`
- Flex children need `min-w-0` to allow text truncation
- Handle empty states—don't render broken UI for empty strings/arrays
- User-generated content: anticipate short, average, and very long inputs
### Images
- `<img>` needs explicit `width` and `height` (prevents CLS)
- Below-fold images: `loading="lazy"`
- Above-fold critical images: `priority` or `fetchpriority="high"`
### Performance
- Large lists (>50 items): virtualize (`virtua`, `content-visibility: auto`)
- No layout reads in render (`getBoundingClientRect`, `offsetHeight`, `offsetWidth`, `scrollTop`)
- Batch DOM reads/writes; avoid interleaving
- Prefer uncontrolled inputs; controlled inputs must be cheap per keystroke
- Add `<link rel="preconnect">` for CDN/asset domains
- Critical fonts: `<link rel="preload" as="font">` with `font-display: swap`
### Navigation & State
- URL reflects state—filters, tabs, pagination, expanded panels in query params
- Links use `<a>`/`<Link>` (Cmd/Ctrl+click, middle-click support)
- Deep-link all stateful UI (if uses `useState`, consider URL sync via nuqs or similar)
- Destructive actions need confirmation modal or undo window—never immediate
### Touch & Interaction
- `touch-action: manipulation` (prevents double-tap zoom delay)
- `-webkit-tap-highlight-color` set intentionally
- `overscroll-behavior: contain` in modals/drawers/sheets
- During drag: disable text selection, `inert` on dragged elements
- `autoFocus` sparingly—desktop only, single primary input; avoid on mobile
### Safe Areas & Layout
- Full-bleed layouts need `env(safe-area-inset-*)` for notches
- Avoid unwanted scrollbars: `overflow-x-hidden` on containers, fix content overflow
- Flex/grid over JS measurement for layout
### Dark Mode & Theming
- `color-scheme: dark` on `<html>` for dark themes (fixes scrollbar, inputs)
- `<meta name="theme-color">` matches page background
- Native `<select>`: explicit `background-color` and `color` (Windows dark mode)
### Locale & i18n
- Dates/times: use `Intl.DateTimeFormat` not hardcoded formats
- Numbers/currency: use `Intl.NumberFormat` not hardcoded formats
- Detect language via `Accept-Language` / `navigator.languages`, not IP
### Hydration Safety
- Inputs with `value` need `onChange` (or use `defaultValue` for uncontrolled)
- Date/time rendering: guard against hydration mismatch (server vs client)
- `suppressHydrationWarning` only where truly needed
### Hover & Interactive States
- Buttons/links need `hover:` state (visual feedback)
- Interactive states increase contrast: hover/active/focus more prominent than rest
### Content & Copy
- Active voice: "Install the CLI" not "The CLI will be installed"
- Title Case for headings/buttons (Chicago style)
- Numerals for counts: "8 deployments" not "eight"
- Specific button labels: "Save API Key" not "Continue"
- Error messages include fix/next step, not just problem
- Second person; avoid first person
- `&` over "and" where space-constrained
### Anti-patterns (flag these)
- `user-scalable=no` or `maximum-scale=1` disabling zoom
- `onPaste` with `preventDefault`
- `transition: all`
- `outline-none` without focus-visible replacement
- Inline `onClick` navigation without `<a>`
- `<div>` or `<span>` with click handlers (should be `<button>`)
- Images without dimensions
- Large arrays `.map()` without virtualization
- Form inputs without labels
- Icon buttons without `aria-label`
- Hardcoded date/number formats (use `Intl.*`)
- `autoFocus` without clear justification
## Output Format
Group by file. Use `file:line` format (VS Code clickable). Terse findings.
```text
## src/Button.tsx
src/Button.tsx:42 - icon button missing aria-label
src/Button.tsx:18 - input lacks label
src/Button.tsx:55 - animation missing prefers-reduced-motion
src/Button.tsx:67 - transition: all → list properties
## src/Modal.tsx
src/Modal.tsx:12 - missing overscroll-behavior: contain
src/Modal.tsx:34 - "..." → "…"
## src/Card.tsx
✓ pass
```
State issue + location. Skip explanation unless fix non-obvious. No preamble.

81
eslint.config.js Normal file
View File

@@ -0,0 +1,81 @@
import js from '@eslint/js';
import tseslint from 'typescript-eslint';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import jsxA11y from 'eslint-plugin-jsx-a11y';
import prettier from 'eslint-config-prettier';
export default tseslint.config(
// Global ignores
{
ignores: [
'node_modules/',
'storybook-static/',
'src/theme/generated/',
'tokens/export/',
'style-dictionary/',
'*.config.js',
'*.config.ts',
],
},
// Base JS recommended
js.configs.recommended,
// TypeScript recommended
...tseslint.configs.recommended,
// React
{
plugins: { react },
settings: {
react: { version: 'detect' },
},
rules: {
'react/react-in-jsx-scope': 'off', // React 18 JSX transform
'react/prop-types': 'off', // TypeScript handles this
'react/display-name': 'off', // We use manual displayName assignment
},
},
// React Hooks
{
plugins: { 'react-hooks': reactHooks },
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
// Accessibility — critical for FA's elderly/distressed audience
{
plugins: { 'jsx-a11y': jsxA11y },
rules: {
...jsxA11y.configs.recommended.rules,
},
},
// Project-specific rules
{
rules: {
'@typescript-eslint/no-explicit-any': 'error',
'@typescript-eslint/no-unused-vars': ['error', {
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
}],
'no-console': 'warn',
},
},
// Story files — relax rules for Storybook patterns
{
files: ['**/*.stories.tsx', '**/*.stories.ts'],
rules: {
'react-hooks/rules-of-hooks': 'off', // Storybook render functions use hooks but aren't uppercase components
'no-console': 'off', // Console in story actions is expected
},
},
// Prettier must be last — disables rules that conflict with formatting
prettier,
);

5006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -12,30 +12,57 @@
"build:storybook": "storybook build",
"build:tokens": "node style-dictionary/config.js",
"type-check": "tsc --noEmit",
"chromatic": "chromatic --exit-zero-on-changes"
"lint": "eslint src/",
"lint:fix": "eslint src/ --fix",
"format": "prettier --write 'src/**/*.{ts,tsx}'",
"format:check": "prettier --check 'src/**/*.{ts,tsx}'",
"test": "vitest run --passWithNoTests",
"test:watch": "vitest",
"chromatic": "chromatic --exit-zero-on-changes",
"prepare": "husky"
},
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@mui/icons-material": "^5.16.0",
"@mui/material": "^5.16.0",
"@mui/system": "^5.16.0",
"@mui/icons-material": "^5.16.0",
"react": "^18.3.0",
"react-dom": "^18.3.0"
},
"devDependencies": {
"@eslint/js": "^9.39.4",
"@storybook/addon-designs": "^8.0.0",
"@storybook/addon-essentials": "^8.4.0",
"@storybook/blocks": "^8.4.0",
"@storybook/react": "^8.4.0",
"@storybook/react-vite": "^8.4.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.0",
"chromatic": "^11.0.0",
"eslint": "^9.39.4",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1",
"husky": "^9.1.7",
"jsdom": "^29.0.1",
"lint-staged": "^16.4.0",
"prettier": "^3.8.1",
"storybook": "^8.4.0",
"style-dictionary": "^4.2.0",
"typescript": "^5.5.0",
"vite": "^5.4.0"
"typescript-eslint": "^8.57.2",
"vite": "^5.4.0",
"vitest": "^4.1.2"
},
"lint-staged": {
"src/**/*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
]
}
}

View File

@@ -24,37 +24,25 @@ export interface BadgeProps extends Omit<BoxProps, 'color'> {
// ─── Colour maps ─────────────────────────────────────────────────────────────
const filledColors: Record<BadgeColor, (t: Theme) => { bg: string; text: string }> = {
default: (t) => ({ bg: t.palette.grey[700], text: t.palette.common.white }),
brand: (t) => ({ bg: t.palette.primary.main, text: t.palette.common.white }),
success: (t) => ({ bg: t.palette.success.main, text: t.palette.common.white }),
warning: (t) => ({ bg: t.palette.warning.main, text: t.palette.common.white }),
error: (t) => ({ bg: t.palette.error.main, text: t.palette.common.white }),
info: (t) => ({ bg: t.palette.info.main, text: t.palette.common.white }),
const filledColors: Record<BadgeColor, { bg: string; text: string }> = {
default: { bg: 'var(--fa-color-neutral-700)', text: 'var(--fa-color-white)' },
brand: { bg: 'var(--fa-color-interactive-default)', text: 'var(--fa-color-white)' },
success: { bg: 'var(--fa-color-feedback-success)', text: 'var(--fa-color-white)' },
warning: { bg: 'var(--fa-color-feedback-warning)', text: 'var(--fa-color-white)' },
error: { bg: 'var(--fa-color-feedback-error)', text: 'var(--fa-color-white)' },
info: { bg: 'var(--fa-color-feedback-info)', text: 'var(--fa-color-white)' },
};
const softColors: Record<BadgeColor, (t: Theme) => { bg: string; text: string }> = {
default: (t) => ({ bg: t.palette.grey[200], text: t.palette.grey[700] }),
brand: () => ({
bg: 'var(--fa-color-brand-200)',
text: 'var(--fa-color-brand-700)',
}),
success: () => ({
const softColors: Record<BadgeColor, { bg: string; text: string }> = {
default: { bg: 'var(--fa-color-neutral-200)', text: 'var(--fa-color-neutral-700)' },
brand: { bg: 'var(--fa-color-brand-200)', text: 'var(--fa-color-brand-700)' },
success: {
bg: 'var(--fa-color-feedback-success-subtle)',
text: 'var(--fa-color-feedback-success)',
}),
warning: () => ({
bg: 'var(--fa-color-feedback-warning-subtle)',
text: 'var(--fa-color-text-warning)',
}),
error: () => ({
bg: 'var(--fa-color-feedback-error-subtle)',
text: 'var(--fa-color-feedback-error)',
}),
info: () => ({
bg: 'var(--fa-color-feedback-info-subtle)',
text: 'var(--fa-color-feedback-info)',
}),
},
warning: { bg: 'var(--fa-color-feedback-warning-subtle)', text: 'var(--fa-color-text-warning)' },
error: { bg: 'var(--fa-color-feedback-error-subtle)', text: 'var(--fa-color-feedback-error)' },
info: { bg: 'var(--fa-color-feedback-info-subtle)', text: 'var(--fa-color-feedback-info)' },
};
// ─── Component ───────────────────────────────────────────────────────────────
@@ -82,18 +70,7 @@ const softColors: Record<BadgeColor, (t: Theme) => { bg: string; text: string }>
* provide an `aria-label` prop so screen readers can announce the status.
*/
export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
(
{
color = 'default',
variant = 'soft',
size = 'medium',
icon,
children,
sx,
...props
},
ref,
) => {
({ color = 'default', variant = 'soft', size = 'medium', icon, children, sx, ...props }, ref) => {
const sizeMap = {
small: {
height: 'var(--fa-badge-height-sm)',
@@ -123,9 +100,7 @@ export const Badge = React.forwardRef<HTMLDivElement, BadgeProps>(
component="span"
sx={[
(theme: Theme) => {
const colors = variant === 'filled'
? filledColors[color](theme)
: softColors[color](theme);
const colors = variant === 'filled' ? filledColors[color] : softColors[color];
return {
display: 'inline-flex',

View File

@@ -5,7 +5,7 @@ import type { DividerProps as MuiDividerProps } from '@mui/material/Divider';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Divider component */
export interface DividerProps extends MuiDividerProps {}
export type DividerProps = MuiDividerProps;
// ─── Component ───────────────────────────────────────────────────────────────
@@ -31,11 +31,9 @@ export interface DividerProps extends MuiDividerProps {}
* <Divider variant="inset" />
* ```
*/
export const Divider = React.forwardRef<HTMLHRElement, DividerProps>(
(props, ref) => {
return <MuiDivider ref={ref} {...props} />;
},
);
export const Divider = React.forwardRef<HTMLHRElement, DividerProps>((props, ref) => {
return <MuiDivider ref={ref} {...props} />;
});
Divider.displayName = 'Divider';
export default Divider;

View File

@@ -5,7 +5,7 @@ import type { IconButtonProps as MuiIconButtonProps } from '@mui/material/IconBu
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA IconButton component */
export interface IconButtonProps extends MuiIconButtonProps {}
export type IconButtonProps = MuiIconButtonProps;
// ─── Component ───────────────────────────────────────────────────────────────
@@ -30,11 +30,9 @@ export interface IconButtonProps extends MuiIconButtonProps {}
* </IconButton>
* ```
*/
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>(
(props, ref) => {
return <MuiIconButton ref={ref} {...props} />;
},
);
export const IconButton = React.forwardRef<HTMLButtonElement, IconButtonProps>((props, ref) => {
return <MuiIconButton ref={ref} {...props} />;
});
IconButton.displayName = 'IconButton';
export default IconButton;

View File

@@ -5,7 +5,7 @@ import type { LinkProps as MuiLinkProps } from '@mui/material/Link';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Link component */
export interface LinkProps extends MuiLinkProps {}
export type LinkProps = MuiLinkProps;
// ─── Component ───────────────────────────────────────────────────────────────
@@ -28,11 +28,9 @@ export interface LinkProps extends MuiLinkProps {}
* For button-styled links, use `Button` with `component="a"` and `href`.
* For navigation menu items, use Link with `underline="none"`.
*/
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>(
(props, ref) => {
return <MuiLink ref={ref} {...props} />;
},
);
export const Link = React.forwardRef<HTMLAnchorElement, LinkProps>((props, ref) => {
return <MuiLink ref={ref} {...props} />;
});
Link.displayName = 'Link';
export default Link;

View File

@@ -5,7 +5,7 @@ import type { RadioProps as MuiRadioProps } from '@mui/material/Radio';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Radio component */
export interface RadioProps extends MuiRadioProps {}
export type RadioProps = MuiRadioProps;
// ─── Component ───────────────────────────────────────────────────────────────
@@ -32,11 +32,9 @@ export interface RadioProps extends MuiRadioProps {}
* </RadioGroup>
* ```
*/
export const Radio = React.forwardRef<HTMLButtonElement, RadioProps>(
(props, ref) => {
return <MuiRadio ref={ref} {...props} />;
},
);
export const Radio = React.forwardRef<HTMLButtonElement, RadioProps>((props, ref) => {
return <MuiRadio ref={ref} {...props} />;
});
Radio.displayName = 'Radio';
export default Radio;

View File

@@ -5,7 +5,7 @@ import type { SwitchProps as MuiSwitchProps } from '@mui/material/Switch';
// ─── Types ───────────────────────────────────────────────────────────────────
/** Props for the FA Switch component */
export interface SwitchProps extends MuiSwitchProps {}
export type SwitchProps = MuiSwitchProps;
// ─── Component ───────────────────────────────────────────────────────────────
@@ -29,11 +29,9 @@ export interface SwitchProps extends MuiSwitchProps {}
* <FormControlLabel control={<Switch />} label="Include catering" />
* ```
*/
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>(
(props, ref) => {
return <MuiSwitch ref={ref} {...props} />;
},
);
export const Switch = React.forwardRef<HTMLButtonElement, SwitchProps>((props, ref) => {
return <MuiSwitch ref={ref} {...props} />;
});
Switch.displayName = 'Switch';
export default Switch;

1
src/test/setup.ts Normal file
View File

@@ -0,0 +1 @@
import '@testing-library/jest-dom/vitest';

19
vitest.config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { defineConfig } from 'vitest/config';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
test: {
environment: 'jsdom',
include: ['src/**/*.test.{ts,tsx}'],
setupFiles: ['./src/test/setup.ts'],
},
resolve: {
alias: {
'@atoms': path.resolve(__dirname, 'src/components/atoms'),
'@molecules': path.resolve(__dirname, 'src/components/molecules'),
'@organisms': path.resolve(__dirname, 'src/components/organisms'),
},
},
});