Compare commits
10 Commits
b9e0d9ad13
...
db9d1ba603
| Author | SHA1 | Date | |
|---|---|---|---|
| db9d1ba603 | |||
| 68889af9c2 | |||
| eb6cf6a185 | |||
| aaa4f80a05 | |||
| 90f47580ca | |||
| ad589b9870 | |||
| f255e8f26e | |||
| b2f0d8af5c | |||
| ecf7a13e1d | |||
| 73aaa71644 |
@@ -1,82 +0,0 @@
|
||||
# Component Builder
|
||||
|
||||
You are the component-builder agent for the FA Design System. Your responsibility is building React components that consume the MUI theme and follow all project conventions.
|
||||
|
||||
## Before starting
|
||||
|
||||
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||
2. Read `docs/memory/decisions-log.md` — don't contradict previous decisions
|
||||
3. Read `docs/memory/component-registry.md` — check status, avoid duplicates
|
||||
4. Read `docs/memory/token-registry.md` — know which tokens are available
|
||||
5. Read `docs/conventions/component-conventions.md` — follow all rules
|
||||
6. Read `docs/design-system.md` — understand the spec for this component
|
||||
|
||||
## Your workflow
|
||||
|
||||
### Pre-flight checks
|
||||
|
||||
Before writing any code:
|
||||
1. **Dependency check for molecules** — if building a molecule, confirm all constituent atoms are marked `done` in `docs/memory/component-registry.md`. If any are `planned` or `in-progress`, STOP and tell the user which atoms need to be built first.
|
||||
2. **Dependency check for organisms** — if building an organism, confirm all constituent molecules and atoms are `done`.
|
||||
3. **Token check** — confirm `docs/memory/token-registry.md` has populated token entries. If tokens haven't been created yet (all sections empty), STOP and tell the user to run `/create-tokens` first.
|
||||
|
||||
### Building a component
|
||||
|
||||
1. **Check the registry** — confirm the component is planned and not already in progress. If it's `in-progress`, STOP and ask the user if they want to continue or restart it.
|
||||
2. **Update the registry** — mark status as `in-progress`
|
||||
3. **Create the component folder:**
|
||||
```
|
||||
src/components/{tier}/{ComponentName}/
|
||||
├── {ComponentName}.tsx
|
||||
├── {ComponentName}.stories.tsx
|
||||
└── index.ts
|
||||
```
|
||||
4. **Build the component** following all conventions:
|
||||
- Extend appropriate MUI base component props
|
||||
- ALL visual values from `theme` — never hardcode colours, spacing, typography, shadows
|
||||
- Use `styled()` with `shouldForwardProp` for custom props
|
||||
- Export both the component and props interface
|
||||
- Include JSDoc on every prop
|
||||
- Use `React.forwardRef` for interactive elements
|
||||
- Minimum 44px touch target on mobile
|
||||
- Visible focus indicators
|
||||
5. **Write Storybook stories** covering ALL states from the checklist in `docs/conventions/component-conventions.md`:
|
||||
- Default state with typical content
|
||||
- All visual variants side by side
|
||||
- All sizes side by side (if applicable)
|
||||
- Disabled state
|
||||
- Loading state (if applicable)
|
||||
- Error state (if applicable)
|
||||
- Long content / content overflow
|
||||
- Empty/minimal content
|
||||
- With and without optional elements
|
||||
Every story meta MUST include `tags: ['autodocs']`. Do NOT mark the component done until all applicable stories exist.
|
||||
6. **Create component tokens** in `tokens/component/{component}.json` if the component has stateful visual variants (e.g., `button.background.hover`, `input.border.error`) not covered by semantic tokens. If the component only uses existing semantic tokens, skip this step.
|
||||
7. **Always create the `index.ts`** re-export file — components won't be importable without it:
|
||||
```typescript
|
||||
export { default } from './{ComponentName}';
|
||||
export * from './{ComponentName}';
|
||||
```
|
||||
8. **Verify in Storybook** — check the component renders correctly at http://localhost:6006. If it doesn't render, fix the issue before proceeding.
|
||||
|
||||
### Component rules (non-negotiable)
|
||||
|
||||
- NEVER hardcode colours, spacing, font sizes, shadows, or border radii
|
||||
- Use `theme.palette.*`, `theme.spacing()`, `theme.typography.*`, `theme.shape.*`
|
||||
- Every component MUST accept and forward the `sx` prop
|
||||
- Use `Omit<>` to remove conflicting MUI base props
|
||||
- Disabled elements: 40% opacity, `aria-disabled`, no pointer events
|
||||
- Focus-visible: 2px solid interactive colour, 2px offset
|
||||
|
||||
### Tiers
|
||||
|
||||
- **Atoms** (`src/components/atoms/`): Button, Input, Typography, Badge, Icon, Avatar, Divider, Chip, Card, Link
|
||||
- **Molecules** (`src/components/molecules/`): FormField, PriceCard, ServiceOption, SearchBar, StepIndicator
|
||||
- **Organisms** (`src/components/organisms/`): ServiceSelector, PricingTable, ArrangementForm, Navigation, Footer
|
||||
|
||||
## After completing work
|
||||
|
||||
1. Update `docs/memory/component-registry.md` — mark component status as `review`
|
||||
2. Update `docs/memory/token-registry.md` if you created any component tokens
|
||||
3. Update `docs/memory/decisions-log.md` with any design decisions
|
||||
4. Update `docs/memory/session-log.md` with work summary and next steps
|
||||
@@ -1,70 +0,0 @@
|
||||
# Story Writer
|
||||
|
||||
You are the story-writer agent for the FA Design System. Your responsibility is creating and maintaining Storybook stories that document and showcase components.
|
||||
|
||||
## Before starting
|
||||
|
||||
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||
2. Read `docs/memory/component-registry.md` — know which components exist and their status
|
||||
3. Read `docs/conventions/component-conventions.md` — follow story conventions
|
||||
4. Read the component's source file to understand its props and variants
|
||||
|
||||
## Your workflow
|
||||
|
||||
### Writing stories for a component
|
||||
|
||||
1. **Read the component** — understand all props, variants, and states
|
||||
2. **Create or update** `{ComponentName}.stories.tsx` in the component folder
|
||||
3. **Follow the story template** from `docs/conventions/component-conventions.md`
|
||||
4. **Cover all required states** (see checklist below)
|
||||
5. **Verify in Storybook** at http://localhost:6006
|
||||
|
||||
### Story structure
|
||||
|
||||
```typescript
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentName } from './ComponentName';
|
||||
|
||||
const meta: Meta<typeof ComponentName> = {
|
||||
title: '{Tier}/{ComponentName}', // e.g., 'Atoms/Button'
|
||||
component: ComponentName,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'centered', // 'centered' for atoms, 'padded' or 'fullscreen' for larger
|
||||
},
|
||||
argTypes: {
|
||||
// One entry per prop with control type, options, description
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof ComponentName>;
|
||||
```
|
||||
|
||||
### Coverage checklist (every component MUST have)
|
||||
|
||||
- [ ] **Default** — typical usage with standard content
|
||||
- [ ] **AllVariants** — all visual variants side by side (if applicable)
|
||||
- [ ] **AllSizes** — all size options side by side (if applicable)
|
||||
- [ ] **Disabled** — disabled state
|
||||
- [ ] **Loading** — loading state (if applicable)
|
||||
- [ ] **Error** — error state (if applicable)
|
||||
- [ ] **LongContent** — text overflow / long content handling
|
||||
- [ ] **MinimalContent** — empty or minimal content
|
||||
- [ ] **WithOptionalElements** — with/without icons, badges, etc.
|
||||
|
||||
### Story naming
|
||||
|
||||
- Use PascalCase for story names
|
||||
- Be descriptive of the state or variant shown
|
||||
- Title prefix matches atomic tier: `Atoms/`, `Molecules/`, `Organisms/`
|
||||
|
||||
### Autodocs
|
||||
|
||||
- Always include `tags: ['autodocs']` in meta
|
||||
- Write JSDoc comments on component props — these become the docs
|
||||
- Use `argTypes` to configure controls with descriptions and defaults
|
||||
|
||||
## After completing work
|
||||
|
||||
1. Update `docs/memory/session-log.md` noting which stories were written/updated
|
||||
@@ -1,57 +0,0 @@
|
||||
# Token Architect
|
||||
|
||||
You are the token-architect agent for the FA Design System. Your responsibility is creating and maintaining design tokens — the single source of truth for all visual properties.
|
||||
|
||||
## Before starting
|
||||
|
||||
1. Read `docs/memory/session-log.md` — understand what's been done
|
||||
2. Read `docs/memory/decisions-log.md` — don't contradict previous decisions
|
||||
3. Read `docs/memory/token-registry.md` — know what tokens already exist
|
||||
4. Read `docs/conventions/token-conventions.md` — follow all naming rules
|
||||
5. Read `docs/design-system.md` — understand the brand context and spec
|
||||
|
||||
## Your workflow
|
||||
|
||||
### Creating tokens
|
||||
|
||||
1. **Gather input** — the user provides brand colours, fonts, or reference images
|
||||
2. **Use Figma MCP** if the user provides a Figma URL — call `get_design_context` or `get_screenshot` to extract design values
|
||||
3. **Create primitive tokens** in `tokens/primitives/` — raw hex, px, font names using 50-950 colour scales
|
||||
4. **Create semantic tokens** in `tokens/semantic/` — map primitives to design intent (text, surface, border, interactive, feedback)
|
||||
5. **Validate token format** — before building, check every token has `$value`, `$type`, and `$description`. Missing `$description` is the most common mistake.
|
||||
6. **Run `npm run build:tokens`** to generate CSS custom properties and JS module. If the build fails, read the error output and fix the token JSON before retrying.
|
||||
7. **Update the MUI theme** in `src/theme/index.ts` to consume the generated token values. Common mappings:
|
||||
- `color.brand.primary` → `palette.primary.main`
|
||||
- `color.text.primary` → `palette.text.primary`
|
||||
- `color.surface.default` → `palette.background.default`
|
||||
- `color.feedback.*` → `palette.error.main`, `palette.warning.main`, etc.
|
||||
- `fontFamily.heading` / `fontFamily.body` → `typography.fontFamily`
|
||||
- Import values from `./generated/tokens.js`
|
||||
8. **Verify** the build completes without errors
|
||||
|
||||
### Token rules (non-negotiable)
|
||||
|
||||
- Every token MUST have `$value`, `$type`, and `$description` (W3C DTCG format)
|
||||
- Semantic tokens MUST reference primitives via aliases: `"$value": "{color.blue.700}"`
|
||||
- Component tokens MUST reference semantic tokens
|
||||
- All text colour combinations MUST meet WCAG 2.1 AA contrast (4.5:1 normal, 3:1 large)
|
||||
- Use the `--fa-` CSS custom property prefix
|
||||
|
||||
### File structure
|
||||
|
||||
```
|
||||
tokens/primitives/colours.json — brand, neutral, feedback hue scales
|
||||
tokens/primitives/typography.json — font families, sizes, weights, line heights
|
||||
tokens/primitives/spacing.json — spacing scale, border radius
|
||||
tokens/primitives/effects.json — shadows, opacity
|
||||
tokens/semantic/colours.json — text, surface, border, interactive, feedback mappings
|
||||
tokens/semantic/typography.json — typography role mappings (display, h1, body, etc.)
|
||||
tokens/semantic/spacing.json — layout and component spacing
|
||||
tokens/component/*.json — per-component tokens (created during component building)
|
||||
```
|
||||
|
||||
## After completing work
|
||||
|
||||
1. Update `docs/memory/token-registry.md` with every token you created/modified
|
||||
2. Update `docs/memory/decisions-log.md` with any design decisions and rationale
|
||||
3. Update `docs/memory/session-log.md` with work summary and next steps
|
||||
@@ -1,130 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,120 +0,0 @@
|
||||
---
|
||||
name: audit
|
||||
description: Run technical quality checks across accessibility, performance, theming, responsive design, and anti-patterns. Generates a scored report with P0-P3 severity ratings. Adapted from impeccable (Apache 2.0).
|
||||
user-invocable: true
|
||||
argument-hint: "[component or area to audit]"
|
||||
---
|
||||
|
||||
Run systematic **technical** quality checks on a component or area and generate a scored report. This is assessment-only — don't fix issues, document them.
|
||||
|
||||
**Target:** $ARGUMENTS
|
||||
|
||||
## Preparation
|
||||
|
||||
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
|
||||
|
||||
Score each dimension 0-4.
|
||||
|
||||
### 1. Accessibility (A11y)
|
||||
|
||||
**Check for**:
|
||||
- Contrast ratios < 4.5:1 for text, < 3:1 for large text and UI components
|
||||
- Missing ARIA: interactive elements without proper roles, labels, or states
|
||||
- Keyboard navigation: missing focus-visible indicators, illogical tab order
|
||||
- Semantic HTML: divs instead of buttons, missing landmarks, heading hierarchy
|
||||
- Form issues: inputs without labels, poor error messaging
|
||||
- Touch targets < 44px (critical for FA's audience — elderly, distressed)
|
||||
|
||||
**Score**: 0=Fails WCAG A, 1=Major gaps, 2=Partial effort, 3=AA mostly met, 4=AA fully met
|
||||
|
||||
### 2. Performance
|
||||
|
||||
**Check for**:
|
||||
- Expensive animations: animating layout properties instead of transform/opacity
|
||||
- Missing optimisation: unoptimised assets, missing lazy loading
|
||||
- Bundle concerns: unnecessary imports, unused dependencies
|
||||
- Render performance: unnecessary re-renders, missing memoisation
|
||||
|
||||
**Score**: 0=Severe issues, 1=Major problems, 2=Partial, 3=Mostly optimised, 4=Excellent
|
||||
|
||||
### 3. Theming & Token Compliance
|
||||
|
||||
**Check for**:
|
||||
- Hardcoded colours not using theme palette or CSS variables
|
||||
- Hardcoded spacing not using theme.spacing() or token values
|
||||
- Hardcoded typography not using theme.typography variants
|
||||
- Inconsistent token usage: wrong tier (primitive instead of semantic)
|
||||
- Component tokens missing `$description` fields
|
||||
|
||||
**Score**: 0=Hardcoded everything, 1=Minimal tokens, 2=Inconsistent, 3=Good with minor gaps, 4=Full token compliance
|
||||
|
||||
### 4. Responsive Design
|
||||
|
||||
**Check for**:
|
||||
- Fixed widths that break on mobile
|
||||
- Touch targets < 44px on interactive elements
|
||||
- Horizontal scroll/overflow on narrow viewports
|
||||
- Text scaling: layouts that break when text size increases
|
||||
- Missing responsive padding (mobile vs desktop)
|
||||
|
||||
**Score**: 0=Desktop-only, 1=Major issues, 2=Partial, 3=Good, 4=Fluid and responsive
|
||||
|
||||
### 5. Design Quality
|
||||
|
||||
**Check against these anti-patterns** (from impeccable frontend-design guidelines):
|
||||
- Gray text on coloured backgrounds (looks washed out)
|
||||
- Cards nested inside cards (visual noise)
|
||||
- Identical card grids with no variation
|
||||
- Bounce/elastic easing (dated, tacky)
|
||||
- Every button styled as primary (no hierarchy)
|
||||
- Redundant copy (headers restating the same info)
|
||||
- Glassmorphism/blur used decoratively
|
||||
- Missing interactive states (hover without focus, or vice versa)
|
||||
|
||||
**Score**: 0=Multiple anti-patterns, 1=Several issues, 2=A couple, 3=Mostly clean, 4=Intentional, distinctive design
|
||||
|
||||
## Report Format
|
||||
|
||||
### Audit Health Score
|
||||
|
||||
| # | Dimension | Score | Key Finding |
|
||||
|---|-----------|-------|-------------|
|
||||
| 1 | Accessibility | ? | |
|
||||
| 2 | Performance | ? | |
|
||||
| 3 | Theming & Tokens | ? | |
|
||||
| 4 | Responsive Design | ? | |
|
||||
| 5 | Design Quality | ? | |
|
||||
| **Total** | | **??/20** | **[Rating]** |
|
||||
|
||||
**Ratings**: 18-20 Excellent, 14-17 Good, 10-13 Acceptable, 6-9 Poor, 0-5 Critical
|
||||
|
||||
### Executive Summary
|
||||
- Score and rating
|
||||
- Issue count by severity (P0/P1/P2/P3)
|
||||
- Top 3-5 critical issues
|
||||
- Recommended next steps
|
||||
|
||||
### Detailed Findings
|
||||
|
||||
Tag each issue **P0-P3**:
|
||||
- **P0 Blocking**: Prevents task completion — fix immediately
|
||||
- **P1 Major**: WCAG AA violation or significant UX issue — fix before release
|
||||
- **P2 Minor**: Annoyance, workaround exists — fix in next pass
|
||||
- **P3 Polish**: Nice-to-fix — address if time permits
|
||||
|
||||
For each issue:
|
||||
- **[P?] Issue name**
|
||||
- **Location**: Component, file, line
|
||||
- **Category**: Accessibility / Performance / Theming / Responsive / Design Quality
|
||||
- **Impact**: How it affects users
|
||||
- **Recommendation**: How to fix it
|
||||
|
||||
### Positive Findings
|
||||
Note what's working well — good practices to maintain.
|
||||
|
||||
**NEVER**: Report issues without explaining impact. Provide generic recommendations. Skip positive findings. Mark everything as P0.
|
||||
@@ -1,28 +0,0 @@
|
||||
---
|
||||
name: build-atom
|
||||
description: Build an atom component (Button, Input, Typography, etc.)
|
||||
argument-hint: "[ComponentName]"
|
||||
---
|
||||
|
||||
Build an atom component for the FA Design System.
|
||||
|
||||
Use the component-builder agent to handle this task. The user wants to build the following atom component:
|
||||
|
||||
**Component:** $ARGUMENTS
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read all memory files and conventions before starting
|
||||
2. Check `docs/memory/component-registry.md` to confirm the component is planned
|
||||
3. Create the component in `src/components/atoms/{ComponentName}/`
|
||||
4. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||
5. Follow all rules in `docs/conventions/component-conventions.md`
|
||||
6. ALL visual values MUST come from the MUI theme — never hardcode
|
||||
7. Write comprehensive Storybook stories covering all states
|
||||
8. Verify the component renders in Storybook
|
||||
9. **Run internal QA** (stages 3-5 from `docs/reference/component-lifecycle.md`):
|
||||
- Run `/audit` — fix all P0/P1 issues
|
||||
- Run `/critique` (skip for simple utility atoms like Divider)
|
||||
- Run `/harden` (skip for display-only atoms like Typography, Badge)
|
||||
- Run `/polish` — final alignment, spacing, transitions
|
||||
10. Present to user with summary and scores
|
||||
11. Update all memory files when done
|
||||
@@ -1,30 +0,0 @@
|
||||
---
|
||||
name: build-molecule
|
||||
description: Build a molecule component (PriceCard, FormField, etc.)
|
||||
argument-hint: "[ComponentName]"
|
||||
---
|
||||
|
||||
Build a molecule component for the FA Design System.
|
||||
|
||||
Use the component-builder agent to handle this task. The user wants to build the following molecule component:
|
||||
|
||||
**Component:** $ARGUMENTS
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read all memory files and conventions before starting
|
||||
2. Check `docs/memory/component-registry.md` to confirm the component is planned and that its constituent atoms are `done`
|
||||
3. Create the component in `src/components/molecules/{ComponentName}/`
|
||||
4. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||
5. Compose from existing atom components — import from `@atoms/`
|
||||
6. Follow all rules in `docs/conventions/component-conventions.md`
|
||||
7. ALL visual values MUST come from the MUI theme — never hardcode
|
||||
8. Write comprehensive Storybook stories with realistic content
|
||||
9. Verify the component renders in Storybook
|
||||
10. **Run internal QA** (stages 3-5 from `docs/reference/component-lifecycle.md`):
|
||||
- Run `/audit` — fix all P0/P1 issues
|
||||
- Run `/critique` — fix all P0/P1 issues
|
||||
- Run `/harden` — check edge cases for interactive molecules
|
||||
- Run `/polish` — final alignment, spacing, transitions
|
||||
- Run `/typeset` if text-heavy (cards, form fields)
|
||||
11. Present to user with summary and scores
|
||||
12. Update all memory files when done
|
||||
@@ -1,34 +0,0 @@
|
||||
---
|
||||
name: build-organism
|
||||
description: Build an organism component (Navigation, PricingTable, etc.)
|
||||
argument-hint: "[ComponentName]"
|
||||
---
|
||||
|
||||
Build an organism component for the FA Design System.
|
||||
|
||||
Use the component-builder agent to handle this task. The user wants to build the following organism component:
|
||||
|
||||
**Component:** $ARGUMENTS
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read all memory files and conventions before starting
|
||||
2. Check `docs/memory/component-registry.md` — confirm the organism is planned
|
||||
3. Verify all constituent molecules and atoms are marked `done` in the registry — if any are not, STOP and tell the user which dependencies need to be built first
|
||||
4. Create the component in `src/components/organisms/{ComponentName}/`
|
||||
5. Include: `{ComponentName}.tsx`, `{ComponentName}.stories.tsx`, `index.ts`
|
||||
6. Compose from existing molecule and atom components — import from `@molecules/` and `@atoms/`
|
||||
7. Follow all rules in `docs/conventions/component-conventions.md`
|
||||
8. ALL visual values MUST come from the MUI theme — never hardcode
|
||||
9. Write comprehensive Storybook stories with realistic page-level content
|
||||
10. Verify the component renders in Storybook
|
||||
11. **Run internal QA** (stages 3-5 from `docs/reference/component-lifecycle.md`):
|
||||
- Run `/audit` — fix all P0/P1 issues
|
||||
- Run `/critique` — fix all P0/P1 issues
|
||||
- Run `/harden` — check all edge cases (organisms handle real data)
|
||||
- Run `/polish` — final alignment, spacing, transitions
|
||||
- Run `/typeset` if text-heavy
|
||||
- Run `/adapt` — responsive check (organisms are layout components)
|
||||
- Run `/quieter` if the organism handles sensitive moments (pricing, forms, errors)
|
||||
- Run `/clarify` if the organism has decision points or complex information
|
||||
12. Present to user with summary and scores
|
||||
13. Update all memory files when done
|
||||
@@ -1,150 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: create-tokens
|
||||
description: Create design tokens from brand colours, fonts, and reference material
|
||||
argument-hint: "[brand colours, fonts, or Figma URL]"
|
||||
---
|
||||
|
||||
Create design tokens for the FA Design System.
|
||||
|
||||
Use the token-architect agent to handle this task. The user's input follows — it may include brand colours, font choices, reference images, or Figma URLs.
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read all memory files and conventions before starting
|
||||
2. If the user provides a Figma URL, use the Figma MCP to extract design context
|
||||
3. Create primitive tokens (colour scales, typography, spacing, effects)
|
||||
4. Create semantic tokens (map primitives to design intent)
|
||||
5. Run `npm run build:tokens` to generate outputs
|
||||
6. Update the MUI theme in `src/theme/index.ts` to use generated values
|
||||
7. Update all memory files when done
|
||||
|
||||
User input: $ARGUMENTS
|
||||
@@ -1,124 +0,0 @@
|
||||
---
|
||||
name: critique
|
||||
description: UX design review — evaluates visual hierarchy, emotional journey, cognitive load, and overall quality using Nielsen's heuristics (scored 0-40). Adapted from impeccable (Apache 2.0).
|
||||
user-invocable: true
|
||||
argument-hint: "[component or area to critique]"
|
||||
---
|
||||
|
||||
Conduct a holistic UX design critique. Think like a design director giving feedback — evaluate whether the interface actually works as a designed experience, not just technically.
|
||||
|
||||
**Target:** $ARGUMENTS
|
||||
|
||||
## Preparation
|
||||
|
||||
1. Read `docs/design-system.md` for FA design context and brand guidelines
|
||||
2. Read `docs/memory/decisions-log.md` for design rationale
|
||||
3. Read the target component/area source and stories
|
||||
4. Reference `docs/reference/impeccable/cognitive-load.md` for the 8-item checklist
|
||||
5. Reference `docs/reference/impeccable/heuristics-scoring.md` for scoring criteria
|
||||
|
||||
**FA context reminder**: Funeral Arranger serves families often in grief or distress. The design must feel warm, trustworthy, transparent, and calm. Clarity over cleverness. Accessibility is critical — users may be elderly, emotional, or unfamiliar with technology.
|
||||
|
||||
## Phase 1: Design Critique
|
||||
|
||||
Evaluate across these dimensions:
|
||||
|
||||
### 1. Visual Hierarchy
|
||||
- Does the eye flow to the most important element first?
|
||||
- Is there a clear primary action visible within 2 seconds?
|
||||
- Do size, colour, and position communicate importance correctly?
|
||||
- Is there visual competition between elements of different weights?
|
||||
|
||||
### 2. Information Architecture & Cognitive Load
|
||||
- Is the structure intuitive for a first-time user?
|
||||
- Is related content grouped logically?
|
||||
- Are there too many choices at once? (>4 at a decision point = flag it)
|
||||
- **Run the 8-item cognitive load checklist** from `docs/reference/impeccable/cognitive-load.md`
|
||||
- Report failure count: 0-1 = low (good), 2-3 = moderate, 4+ = critical
|
||||
|
||||
### 3. Emotional Journey
|
||||
- Does the interface feel warm and trustworthy (appropriate for FA)?
|
||||
- Would a grieving family member feel "this is for me"?
|
||||
- Are there design interventions at anxiety-prone moments (pricing, commitment, forms)?
|
||||
- Does the experience end well (confirmation, clear next step)?
|
||||
|
||||
### 4. Discoverability & Affordance
|
||||
- Are interactive elements obviously interactive?
|
||||
- Would a user know what to do without instructions?
|
||||
- Are hover/focus states providing useful feedback?
|
||||
|
||||
### 5. Composition & Balance
|
||||
- Does the layout feel balanced?
|
||||
- Is whitespace used intentionally?
|
||||
- Is there visual rhythm in spacing and repetition?
|
||||
|
||||
### 6. Typography as Communication
|
||||
- Does the type hierarchy signal what to read first, second, third?
|
||||
- Is body text comfortable to read? (line length 45-75ch, adequate size)
|
||||
- Do font choices reinforce FA's warm, professional tone?
|
||||
|
||||
### 7. Colour with Purpose
|
||||
- Is colour used to communicate, not just decorate?
|
||||
- Does the warm gold/copper brand palette feel cohesive?
|
||||
- Are accent colours drawing attention to the right things?
|
||||
- Does it work for colourblind users?
|
||||
|
||||
### 8. States & Edge Cases
|
||||
- Empty states: Do they guide users toward action?
|
||||
- Loading states: Do they reduce perceived wait time?
|
||||
- Error states: Are they helpful and non-blaming? (critical for FA — no aggressive red labels)
|
||||
- Success states: Do they confirm and guide next steps?
|
||||
|
||||
### 9. Microcopy & Voice
|
||||
- Is the writing clear and concise?
|
||||
- Does it sound warm and professional (FA's tone)?
|
||||
- Are labels and buttons unambiguous?
|
||||
- Does error copy help users fix the problem without distress?
|
||||
|
||||
## Phase 2: Present Findings
|
||||
|
||||
### Design Health Score
|
||||
|
||||
Score each of Nielsen's 10 heuristics 0-4 (consult `docs/reference/impeccable/heuristics-scoring.md`):
|
||||
|
||||
| # | Heuristic | Score | Key Issue |
|
||||
|---|-----------|-------|-----------|
|
||||
| 1 | Visibility of System Status | ? | |
|
||||
| 2 | Match System / Real World | ? | |
|
||||
| 3 | User Control and Freedom | ? | |
|
||||
| 4 | Consistency and Standards | ? | |
|
||||
| 5 | Error Prevention | ? | |
|
||||
| 6 | Recognition Rather Than Recall | ? | |
|
||||
| 7 | Flexibility and Efficiency | ? | |
|
||||
| 8 | Aesthetic and Minimalist Design | ? | |
|
||||
| 9 | Error Recovery | ? | |
|
||||
| 10 | Help and Documentation | ? | |
|
||||
| **Total** | | **??/40** | **[Rating]** |
|
||||
|
||||
**Ratings**: 36-40 Excellent, 28-35 Good, 20-27 Acceptable, 12-19 Poor, 0-11 Critical
|
||||
|
||||
### Overall Impression
|
||||
Brief gut reaction — what works, what doesn't, the single biggest opportunity.
|
||||
|
||||
### What's Working
|
||||
2-3 things done well. Be specific about why they work.
|
||||
|
||||
### Priority Issues
|
||||
3-5 most impactful design problems, ordered by importance.
|
||||
|
||||
For each issue, tag **P0-P3**:
|
||||
- **[P?] What**: Name the problem
|
||||
- **Why it matters**: How it hurts users (especially in FA's sensitive context)
|
||||
- **Fix**: Concrete recommendation
|
||||
|
||||
### FA Audience Check
|
||||
Walk through the primary user flow as:
|
||||
1. **Bereaved family member** (60+, first time, emotional, possibly on mobile)
|
||||
2. **Arrangement planner** (30-50, comparing options, price-sensitive, wants transparency)
|
||||
|
||||
For each persona, list specific pain points found.
|
||||
|
||||
### Minor Observations
|
||||
Quick notes on smaller issues worth addressing.
|
||||
|
||||
**Remember**: Be direct and specific. Say what's wrong AND why it matters. Prioritise ruthlessly. Don't soften criticism — honest feedback ships better design.
|
||||
@@ -1,135 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
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
|
||||
@@ -1,146 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,111 +0,0 @@
|
||||
---
|
||||
name: preflight
|
||||
description: Pre-commit quality check — verifies TypeScript, ESLint, Prettier, Storybook, token sync, and no hardcoded values
|
||||
argument-hint: "[--fix to auto-fix issues]"
|
||||
---
|
||||
|
||||
Run quality checks before committing. Reports pass/fail for each check and blocks commit if critical issues are found.
|
||||
|
||||
**Mode:** $ARGUMENTS (pass `--fix` to attempt auto-fixes, otherwise report-only)
|
||||
|
||||
**Run these checks in order:**
|
||||
|
||||
### 1. TypeScript compilation
|
||||
```bash
|
||||
npx tsc --noEmit 2>&1 | grep -v "tokens.js"
|
||||
```
|
||||
- **Pass:** No errors (the tokens.js declaration warning is a known pre-existing issue — ignore it)
|
||||
- **Fail:** Any other TypeScript errors → report them
|
||||
- **Critical:** Yes — do not commit if this fails
|
||||
|
||||
### 2. Storybook build
|
||||
```bash
|
||||
npx storybook build --quiet 2>&1
|
||||
```
|
||||
- **Pass:** Build succeeds
|
||||
- **Fail:** Build errors → report them
|
||||
- **Critical:** Yes — do not commit if this fails
|
||||
|
||||
### 3. Token sync check
|
||||
Compare timestamps: do the generated outputs (`src/theme/generated/tokens.js`, `src/theme/generated/tokens.css`) have an older modification time than any `tokens/**/*.json` file?
|
||||
```bash
|
||||
# Find newest token JSON file
|
||||
newest_token=$(find tokens/ -name "*.json" -newer src/theme/generated/tokens.js 2>/dev/null | head -5)
|
||||
```
|
||||
- **Pass:** No token JSON files are newer than the generated outputs
|
||||
- **Fail:** Token JSON was modified but outputs weren't regenerated
|
||||
- **Fix:** Run `npm run build:tokens`
|
||||
- **Critical:** Yes — stale generated tokens cause silent bugs
|
||||
|
||||
### 4. Hardcoded values scan
|
||||
Scan component files for hardcoded colours, spacing, and font values that should use tokens:
|
||||
```bash
|
||||
# Check for hex colours in component files (excluding stories and tokens)
|
||||
grep -rn "#[0-9a-fA-F]\{3,8\}" src/components/ --include="*.tsx" --include="*.ts" | grep -v ".stories." | grep -v "// ok-hardcode"
|
||||
```
|
||||
- **Pass:** No hex colours found in component source files (stories are exempt)
|
||||
- **Fail:** Hardcoded values found → report file and line
|
||||
- **Note:** Lines with `// ok-hardcode` comment are exempted (for rare intentional cases)
|
||||
- **Critical:** No — warn but don't block commit
|
||||
|
||||
### 5. Component exports check
|
||||
Verify each component folder has a barrel export (`index.ts`) and the component has `displayName`:
|
||||
```bash
|
||||
# Check for missing index.ts
|
||||
for dir in src/components/atoms/*/; do
|
||||
[ -f "$dir/index.ts" ] || echo "Missing index.ts: $dir"
|
||||
done
|
||||
# Check for missing displayName (exclude stories)
|
||||
find src/components/atoms/ -name "*.tsx" ! -name "*.stories.tsx" | xargs grep -L "displayName" 2>/dev/null
|
||||
```
|
||||
- **Pass:** All component folders have index.ts and components set displayName
|
||||
- **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
|
||||
═══════════════════════════════
|
||||
✓ TypeScript .............. PASS
|
||||
✓ Storybook build ........ PASS
|
||||
✓ Token sync ............. PASS
|
||||
⚠ Hardcoded values ....... WARN (2 issues)
|
||||
✓ Component exports ...... PASS
|
||||
✓ ESLint ................. PASS
|
||||
✓ Prettier ............... PASS
|
||||
───────────────────────────────
|
||||
Result: PASS (safe to commit)
|
||||
```
|
||||
|
||||
Use `PASS`, `FAIL`, or `WARN`. If any critical check fails, the result is `FAIL (do not commit)`. If only warnings, result is `PASS (safe to commit)` with warnings listed.
|
||||
|
||||
If `--fix` was passed, attempt to fix issues automatically (e.g., run `npm run build:tokens` for stale tokens) and re-check.
|
||||
|
||||
### 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):
|
||||
|
||||
- **Transition consistency**: All interactive state changes should use `150ms ease-in-out` (FA convention). Flag mismatches.
|
||||
- **Focus-visible**: Every interactive element should have a `:focus-visible` style. Flag any that rely only on hover.
|
||||
- **Touch targets**: Interactive elements should have `minHeight >= 44px` for the largest size (or the mobile-intended size).
|
||||
- **Spacing consistency**: Padding/gap values should use `theme.spacing()` or token CSS variables, not raw px.
|
||||
|
||||
These are advisory — report as `INFO` lines after the main results. Do not block commit for these.
|
||||
@@ -1,131 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,68 +0,0 @@
|
||||
---
|
||||
name: review-component
|
||||
description: Review a component against design system conventions
|
||||
argument-hint: "[ComponentName]"
|
||||
---
|
||||
|
||||
Review a component against FA Design System conventions and report pass/fail for each check.
|
||||
|
||||
**Component to review:** $ARGUMENTS
|
||||
|
||||
**Instructions:**
|
||||
1. Read `docs/conventions/component-conventions.md` for the rules
|
||||
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:**
|
||||
|
||||
### Code quality
|
||||
- [ ] Component uses TypeScript with proper types
|
||||
- [ ] Props interface exported with JSDoc on every prop
|
||||
- [ ] Uses `React.forwardRef` for interactive elements
|
||||
- [ ] Accepts and forwards `sx` prop
|
||||
- [ ] Uses `shouldForwardProp` for custom props on styled components
|
||||
|
||||
### Theme compliance
|
||||
- [ ] NO hardcoded colours — all from `theme.palette.*`
|
||||
- [ ] NO hardcoded spacing — all from `theme.spacing()`
|
||||
- [ ] NO hardcoded typography — all from `theme.typography.*`
|
||||
- [ ] NO hardcoded shadows — all from `theme.shadows`
|
||||
- [ ] NO hardcoded border radius — all from `theme.shape.*`
|
||||
|
||||
### Accessibility
|
||||
- [ ] Minimum 44px touch target on mobile
|
||||
- [ ] Visible focus indicator (focus-visible)
|
||||
- [ ] Appropriate ARIA attributes
|
||||
- [ ] Disabled state uses `aria-disabled`
|
||||
- [ ] Colour contrast meets WCAG 2.1 AA
|
||||
|
||||
### Storybook coverage
|
||||
- [ ] Default story
|
||||
- [ ] All variants story
|
||||
- [ ] All sizes story (if applicable)
|
||||
- [ ] Disabled state
|
||||
- [ ] Loading state (if applicable)
|
||||
- [ ] Long content / overflow
|
||||
- [ ] autodocs tag present
|
||||
|
||||
### Interactive states (ref: docs/reference/impeccable/interaction-design.md)
|
||||
- [ ] Default (resting) state is styled
|
||||
- [ ] Hover state provides visual feedback (not just cursor change)
|
||||
- [ ] Focus-visible state is distinct from hover (keyboard users never see hover)
|
||||
- [ ] Active/pressed state feels responsive
|
||||
- [ ] Disabled state is visually diminished but still distinguishable
|
||||
- [ ] Transitions use 150ms ease-in-out (FA convention)
|
||||
|
||||
### Design anti-patterns (ref: docs/reference/impeccable/frontend-design-skill.md)
|
||||
- [ ] No grey text on coloured backgrounds (use a shade of the background colour instead)
|
||||
- [ ] No cards nested inside cards (flatten hierarchy with spacing/typography)
|
||||
- [ ] No identical card grids with zero variation (vary content, size, or emphasis)
|
||||
- [ ] No bounce/elastic easing (use ease-out-quart or ease-in-out)
|
||||
- [ ] Not every button is primary (use variant hierarchy: contained > soft > outlined > text)
|
||||
- [ ] No redundant copy (headings don't restate content below them)
|
||||
- [ ] No glassmorphism/blur used purely as decoration
|
||||
- [ ] Whitespace is intentional, not leftover
|
||||
|
||||
**Report format:** List each check with pass/fail and specific issues found. End with a summary and recommended fixes.
|
||||
@@ -1,35 +0,0 @@
|
||||
---
|
||||
name: status
|
||||
description: Report current status of tokens, components, and build health
|
||||
---
|
||||
|
||||
Report the current status of the FA Design System.
|
||||
|
||||
**Instructions:**
|
||||
1. Read `docs/memory/session-log.md` — summarise recent work
|
||||
2. Read `docs/memory/component-registry.md` — count components by status (planned, in-progress, review, done)
|
||||
3. Read `docs/memory/token-registry.md` — summarise token coverage
|
||||
4. Read `docs/memory/decisions-log.md` — count decisions logged
|
||||
5. Check if Storybook is running (curl http://localhost:6006)
|
||||
6. Check if tokens build successfully (`npm run build:tokens`)
|
||||
|
||||
**Report format:**
|
||||
```
|
||||
## FA Design System Status
|
||||
|
||||
### Tokens
|
||||
- Primitives: [count] defined
|
||||
- Semantic: [count] defined
|
||||
- Component: [count] defined
|
||||
|
||||
### Components
|
||||
- Done: [list]
|
||||
- In progress: [list]
|
||||
- Planned: [list]
|
||||
|
||||
### Recent activity
|
||||
- [last session summary]
|
||||
|
||||
### Next steps
|
||||
- [recommended next actions]
|
||||
```
|
||||
@@ -1,22 +0,0 @@
|
||||
---
|
||||
name: sync-tokens
|
||||
description: Rebuild CSS and JS outputs from token JSON sources
|
||||
---
|
||||
|
||||
Synchronise design tokens — rebuild CSS and JS outputs from token JSON sources.
|
||||
|
||||
Use this after token JSON files have been edited manually or after `/create-tokens`. This is a maintenance command — it does NOT create new tokens (use `/create-tokens` for that).
|
||||
|
||||
Use the token-architect agent to handle this task.
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read `docs/memory/token-registry.md` to understand current token state
|
||||
2. Validate all token JSON files have required fields (`$value`, `$type`, `$description`)
|
||||
3. Run `npm run build:tokens` to regenerate:
|
||||
- `src/theme/generated/tokens.css` (CSS custom properties)
|
||||
- `src/theme/generated/tokens.js` (JS ES6 module)
|
||||
- `tokens/export/tokens-flat.json` (flat JSON export)
|
||||
4. Check that `src/theme/index.ts` is consuming the generated tokens correctly
|
||||
5. If any tokens were added/changed since the theme was last updated, update `src/theme/index.ts`
|
||||
6. Report what was generated and any issues found
|
||||
7. Update `docs/memory/token-registry.md` if it's out of date
|
||||
@@ -1,116 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,29 +0,0 @@
|
||||
---
|
||||
name: write-stories
|
||||
description: Write or update Storybook stories for a component
|
||||
argument-hint: "[ComponentName]"
|
||||
---
|
||||
|
||||
Write or update Storybook stories for an existing component.
|
||||
|
||||
Use the story-writer agent to handle this task. The component to document:
|
||||
|
||||
**Component:** $ARGUMENTS
|
||||
|
||||
**Instructions for the agent:**
|
||||
1. Read `docs/conventions/component-conventions.md` for story standards
|
||||
2. Read the component source file at `src/components/` (check atoms/, molecules/, organisms/)
|
||||
3. Create or update `{ComponentName}.stories.tsx` in the component's folder
|
||||
4. Cover ALL items in the story coverage checklist:
|
||||
- [ ] Default state with typical content
|
||||
- [ ] All visual variants side by side
|
||||
- [ ] All sizes side by side (if applicable)
|
||||
- [ ] Disabled state
|
||||
- [ ] Loading state (if applicable)
|
||||
- [ ] Error state (if applicable)
|
||||
- [ ] Long content / content overflow
|
||||
- [ ] Empty/minimal content
|
||||
- [ ] With and without optional elements (icons, badges, etc.)
|
||||
5. Every story meta MUST include `tags: ['autodocs']`
|
||||
6. Verify the component renders correctly in Storybook at http://localhost:6006
|
||||
7. Update `docs/memory/session-log.md` when done
|
||||
@@ -1,101 +0,0 @@
|
||||
---
|
||||
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
|
||||
16
.gitignore
vendored
16
.gitignore
vendored
@@ -7,3 +7,19 @@ tokens/export/
|
||||
.env.*
|
||||
.DS_Store
|
||||
*.tgz
|
||||
|
||||
# Claude / Playwright artifacts
|
||||
.playwright-mcp/
|
||||
.claude/
|
||||
|
||||
# Build logs
|
||||
build-storybook.log
|
||||
|
||||
# Working docs (not for sharing)
|
||||
documentation/
|
||||
DESIGN.md
|
||||
venue-services-snapshot.md
|
||||
temp-db/
|
||||
|
||||
# Root-level screenshots
|
||||
/*.png
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"figma-remote-mcp": {
|
||||
"type": "http",
|
||||
"url": "https://mcp.figma.com/mcp"
|
||||
}
|
||||
}
|
||||
}
|
||||
113
CLAUDE.md
113
CLAUDE.md
@@ -1,113 +0,0 @@
|
||||
# FA 2.0 Design System
|
||||
|
||||
## Project overview
|
||||
|
||||
Rebuilding the Funeral Arranger (funeralarranger.com.au) design system using a
|
||||
code-first approach. Parsons (H.Parsons Funeral Directors) is the client. FA is
|
||||
an Australian funeral planning platform — the design language must be warm,
|
||||
professional, trustworthy, and calm. Users are often in distress.
|
||||
|
||||
## Tech stack
|
||||
|
||||
- React 18 + TypeScript
|
||||
- Material UI (MUI) v5
|
||||
- Storybook 8+ with autodocs
|
||||
- Style Dictionary for token transformation
|
||||
- W3C DTCG token format (2025.10 stable spec)
|
||||
- Chromatic for Storybook hosting (later)
|
||||
|
||||
## Architecture
|
||||
|
||||
**Source of truth:** Token JSON files in `tokens/` (DTCG format)
|
||||
**Flow:** Token JSON → Style Dictionary → MUI theme + CSS vars → React components → Storybook
|
||||
|
||||
### Token tiers
|
||||
1. **Primitives** (`tokens/primitives/`): Raw values — hex, px, font names, scales
|
||||
2. **Semantic** (`tokens/semantic/`): Design intent — `color.text.primary`, `color.surface.default`
|
||||
3. **Component** (`tokens/component/`): Per-component — `button.background.default`
|
||||
|
||||
### Component tiers (atomic design)
|
||||
1. **Atoms** (`src/components/atoms/`): Button, Input, Typography, Badge, Icon, Avatar, Divider, Chip, Card, Link
|
||||
2. **Molecules** (`src/components/molecules/`): SearchBar, PriceCard, ServiceOption, FormField
|
||||
3. **Organisms** (`src/components/organisms/`): ServiceSelector, PricingTable, ArrangementForm, Navigation
|
||||
|
||||
## Critical rules
|
||||
|
||||
1. **Every component MUST consume the MUI theme** — never hardcode colours, spacing, typography, or shadows
|
||||
2. **Every token MUST have a `$description`** — this is how agents maintain context about design intent
|
||||
3. **Always read docs/design-system.md** before creating or modifying anything
|
||||
4. **Always check docs/memory/** before starting work — these files contain decisions and state from previous sessions
|
||||
5. **Always update docs/memory/** after completing work — log what was done, decisions made, and open questions
|
||||
6. **Run `npm run build:tokens`** after any token JSON change
|
||||
7. **Verify in Storybook** before marking any component done
|
||||
8. **Follow the component lifecycle** — see `docs/reference/component-lifecycle.md` for the full quality gate sequence (build → QA → polish → present → iterate → normalize → preflight → commit)
|
||||
9. **Commit and push after completing each unit of work** — see Git workflow below
|
||||
|
||||
## Git workflow
|
||||
|
||||
**Remote:** Gitea at `http://192.168.50.211:3000/richie/ParsonsFA.git` (credentials stored via git credential helper)
|
||||
|
||||
**After completing each unit of work** (a component, a token change, a bug fix, etc.):
|
||||
1. Stage the changed files (`git add` — prefer naming specific files over `git add -A`)
|
||||
2. Commit with a clear message describing what was done
|
||||
3. Push to origin (`git push`)
|
||||
|
||||
This is **not optional** — the user relies on the git history for rollback safety. Each commit should represent a coherent, working state (Storybook builds, TypeScript compiles). Do not batch multiple unrelated changes into a single commit.
|
||||
|
||||
**Commit message format:**
|
||||
```
|
||||
Short summary (imperative mood, <70 chars)
|
||||
|
||||
- Bullet points with detail if needed
|
||||
- Reference decision IDs (D001, D002...) when relevant
|
||||
|
||||
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
**Branch strategy:** All work on `main` for now. Feature branches when the project grows.
|
||||
|
||||
## Memory system
|
||||
|
||||
This project uses structured markdown files for cross-session memory.
|
||||
|
||||
**Before starting any work, read these files:**
|
||||
- `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` — Recent sessions (last 2-3); older sessions in `docs/memory/archive/`
|
||||
|
||||
**Session startup — proactive review pass (do this automatically):**
|
||||
Check `docs/reference/retroactive-review-plan.md` for the current review state. If
|
||||
there are tiers or components that haven't been reviewed yet, run the next review
|
||||
step (typically `/normalize {tier}` or `/audit {component}`) at the start of the
|
||||
session before the user's main request. Keep this to ~30-60 min, then report
|
||||
findings and shift to whatever the user wants to work on. This is "morning
|
||||
housekeeping" — the user has approved this and expects it to happen without asking.
|
||||
|
||||
**After completing work, update:**
|
||||
- The relevant memory files with what changed
|
||||
- `docs/memory/session-log.md` with a summary of what was accomplished and next steps
|
||||
|
||||
## MCP servers
|
||||
|
||||
- **Figma remote MCP** (`figma-remote-mcp`): Read FA 1.0 designs, extract design context
|
||||
- **Storybook MCP** (`storybook`): Query component library for available components and props
|
||||
|
||||
Setup instructions in `docs/reference/mcp-setup.md`.
|
||||
|
||||
## File conventions
|
||||
|
||||
- Component folders: PascalCase (`Button/`, `PriceCard/`)
|
||||
- Token files: camelCase (`colours.json`, `typography.json`)
|
||||
- Each component folder contains: `ComponentName.tsx`, `ComponentName.stories.tsx`, `index.ts`
|
||||
- CSS custom properties prefix: `--fa-` (e.g., `--fa-color-brand-primary`)
|
||||
- MUI theme paths: follow MUI conventions (`palette.primary.main`)
|
||||
|
||||
## Naming conventions for tokens
|
||||
|
||||
See `docs/conventions/token-conventions.md` for the full specification.
|
||||
|
||||
Quick reference:
|
||||
- Primitives: `color.blue.500`, `spacing.4`, `fontSize.base`
|
||||
- Semantic: `color.text.primary`, `color.surface.default`, `color.interactive.default`
|
||||
- Component: `button.background.default`, `button.background.hover`
|
||||
58
GEMINI.md
58
GEMINI.md
@@ -1,58 +0,0 @@
|
||||
# FA 2.0 Design System — Antigravity Rules
|
||||
|
||||
## Project context
|
||||
|
||||
Funeral Arranger (funeralarranger.com.au) design system rebuild. Parsons is the
|
||||
client. The design language must be warm, professional, trustworthy, and calm.
|
||||
Users are often in distress.
|
||||
|
||||
## Tech stack
|
||||
|
||||
- React 18 + TypeScript
|
||||
- Material UI (MUI) v5
|
||||
- Storybook 8+ (running on localhost:6006)
|
||||
- Style Dictionary for token transformation
|
||||
- W3C DTCG token format
|
||||
|
||||
## Hard rules
|
||||
|
||||
1. **Never hardcode colours, spacing, typography, or shadows** — always consume the MUI theme (`theme.palette.*`, `theme.spacing()`, `theme.typography.*`) or CSS custom properties (`var(--fa-*)`)
|
||||
2. **Never use raw hex values in components** — map to semantic tokens
|
||||
3. **Follow atomic design tiers:** atoms → molecules → organisms → templates → pages
|
||||
4. **Component folders:** PascalCase, each contains `ComponentName.tsx`, `ComponentName.stories.tsx`, `index.ts`
|
||||
5. **Token files:** camelCase in `tokens/` directory (primitives → semantic → component tiers)
|
||||
6. **CSS custom properties prefix:** `--fa-`
|
||||
7. **Error styling uses copper (#B0610F)** not red — this is intentional for grief-sensitive context
|
||||
8. **Copy tone:** warm but not gushy. Understated empathy. Let UI structure be the empathy.
|
||||
|
||||
## Token access convention
|
||||
|
||||
- Semantic tokens: `theme.palette.*`, `theme.typography.*`, `theme.spacing()`
|
||||
- Component tokens: `var(--fa-component-property-state)`
|
||||
- Never mix — semantic via theme, component via CSS vars
|
||||
|
||||
## Storybook
|
||||
|
||||
Dev server runs on `localhost:6006`. When verifying components visually, navigate
|
||||
to the specific story URL. Story IDs follow the pattern:
|
||||
`/story/{tier}-{componentname}--{variantname}`
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
src/components/atoms/ — Button, Input, Typography, Card, etc.
|
||||
src/components/molecules/ — SearchBar, PriceCard, ServiceOption, etc.
|
||||
src/components/organisms/ — Navigation, Footer, ArrangementDialog, etc.
|
||||
src/components/templates/ — WizardLayout
|
||||
src/components/pages/ — IntroStep, ProvidersStep, etc. (wizard steps)
|
||||
tokens/primitives/ — Raw values (hex, px, font names)
|
||||
tokens/semantic/ — Design intent (color.text.primary)
|
||||
tokens/component/ — Per-component tokens
|
||||
```
|
||||
|
||||
## When making visual changes
|
||||
|
||||
1. Make the code change
|
||||
2. Open the Storybook story in the browser to verify
|
||||
3. Screenshot and check spacing, alignment, visual weight
|
||||
4. Self-correct if something looks off before presenting
|
||||
106
QUICKSTART.md
106
QUICKSTART.md
@@ -1,106 +0,0 @@
|
||||
# FA Design System — Quick Start
|
||||
|
||||
## What you've got
|
||||
|
||||
A complete Claude Code project scaffold with:
|
||||
|
||||
- **3 agents**: token-architect, component-builder, story-writer
|
||||
- **6 slash commands**: /create-tokens, /build-atom, /build-molecule, /sync-tokens, /status, /review-component
|
||||
- **4 memory files**: session-log, decisions-log, component-registry, token-registry
|
||||
- **2 convention docs**: token-conventions, component-conventions
|
||||
- **Living design system spec**: docs/design-system.md
|
||||
- **MCP config**: Figma remote MCP pre-configured
|
||||
- **Full React + MUI + Storybook + Style Dictionary setup**
|
||||
|
||||
## Setup (5-10 minutes)
|
||||
|
||||
```bash
|
||||
# 1. Extract the project
|
||||
tar -xzf fa-design-system-scaffold.tar.gz
|
||||
cd fa-project
|
||||
|
||||
# 2. Run the bootstrap script
|
||||
chmod +x bootstrap.sh
|
||||
./bootstrap.sh
|
||||
|
||||
# 3. Set up Figma MCP (in Claude Code)
|
||||
claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp
|
||||
# Restart Claude Code, then /mcp → authenticate with Figma
|
||||
|
||||
# 4. Start Storybook (separate terminal)
|
||||
npm run storybook
|
||||
```
|
||||
|
||||
## Your workflow
|
||||
|
||||
### Step 1: Create tokens
|
||||
```
|
||||
claude
|
||||
/create-tokens I want to create the FA design system. Here are my brand colours: [paste hex values or attach reference images]. Fonts: [your font choices]. The platform serves Australian families planning funerals — warm, trustworthy, calm aesthetic.
|
||||
```
|
||||
|
||||
### Step 2: Build atoms (one at a time)
|
||||
```
|
||||
/build-atom Button
|
||||
# Review in Storybook at http://localhost:6006
|
||||
# Provide feedback, iterate
|
||||
|
||||
/build-atom Input
|
||||
/build-atom Typography
|
||||
/build-atom Card
|
||||
# ... etc
|
||||
```
|
||||
|
||||
### Step 3: Build molecules
|
||||
```
|
||||
/build-molecule PriceCard
|
||||
/build-molecule FormField
|
||||
# ... etc
|
||||
```
|
||||
|
||||
### Check status anytime
|
||||
```
|
||||
/status
|
||||
```
|
||||
|
||||
### Review a component
|
||||
```
|
||||
/review-component Button
|
||||
```
|
||||
|
||||
## How memory works
|
||||
|
||||
Every agent reads these files before starting work and updates them after:
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `docs/memory/session-log.md` | What happened last session, what's next |
|
||||
| `docs/memory/decisions-log.md` | Every design decision with rationale |
|
||||
| `docs/memory/component-registry.md` | Status of every component |
|
||||
| `docs/memory/token-registry.md` | All tokens with values and usage |
|
||||
|
||||
This means you can close Claude Code, come back tomorrow, and the agents
|
||||
will pick up exactly where you left off.
|
||||
|
||||
## File structure
|
||||
|
||||
```
|
||||
fa-project/
|
||||
├── CLAUDE.md ← Claude Code reads this every session
|
||||
├── .claude/
|
||||
│ ├── agents/ ← Agent definitions
|
||||
│ ├── commands/ ← Slash commands
|
||||
│ └── skills/ ← Shared knowledge
|
||||
├── .mcp.json ← MCP server config
|
||||
├── docs/
|
||||
│ ├── memory/ ← Cross-session memory
|
||||
│ ├── conventions/ ← Rules agents follow
|
||||
│ ├── reference/ ← Setup guides
|
||||
│ └── design-system.md ← Living design spec
|
||||
├── tokens/ ← DTCG token JSON (source of truth)
|
||||
├── style-dictionary/ ← Token build config
|
||||
├── src/
|
||||
│ ├── theme/ ← MUI theme + generated CSS vars
|
||||
│ └── components/ ← Atoms, molecules, organisms
|
||||
└── .storybook/ ← Storybook config with theme provider
|
||||
```
|
||||
138
bootstrap.sh
138
bootstrap.sh
@@ -1,138 +0,0 @@
|
||||
#!/bin/bash
|
||||
# FA Design System — Bootstrap Script
|
||||
# Run this once after cloning/copying the project scaffold to set everything up.
|
||||
|
||||
set -e
|
||||
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ FA Design System — Project Bootstrap ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
|
||||
# ─── 1. Install dependencies ─────────────────────────────────────────────────
|
||||
echo "📦 Installing dependencies..."
|
||||
npm install
|
||||
|
||||
# ─── 2. Initialise Storybook (if not already configured) ─────────────────────
|
||||
echo ""
|
||||
echo "📖 Checking Storybook setup..."
|
||||
if [ ! -d ".storybook" ]; then
|
||||
echo " .storybook config not found. Creating configuration..."
|
||||
mkdir -p .storybook
|
||||
cat > .storybook/main.ts << 'MAINEOF'
|
||||
import type { StorybookConfig } from '@storybook/react-vite';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: ['../src/**/*.stories.@(ts|tsx)'],
|
||||
addons: [
|
||||
'@storybook/addon-essentials',
|
||||
],
|
||||
framework: {
|
||||
name: '@storybook/react-vite',
|
||||
options: {},
|
||||
},
|
||||
docs: {
|
||||
autodocs: 'tag',
|
||||
},
|
||||
viteFinal: async (config) => {
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
MAINEOF
|
||||
cat > .storybook/preview.tsx << 'PREVEOF'
|
||||
import React from 'react';
|
||||
import type { Preview } from '@storybook/react';
|
||||
import { ThemeProvider } from '@mui/material/styles';
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import { theme } from '../src/theme';
|
||||
|
||||
const preview: Preview = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<ThemeProvider theme={theme}>
|
||||
<CssBaseline />
|
||||
<Story />
|
||||
</ThemeProvider>
|
||||
),
|
||||
],
|
||||
parameters: {
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/i,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default preview;
|
||||
PREVEOF
|
||||
echo " ✓ .storybook config created."
|
||||
else
|
||||
echo " .storybook config found. Skipping."
|
||||
fi
|
||||
|
||||
# ─── 3. Build initial token output ───────────────────────────────────────────
|
||||
echo ""
|
||||
echo "🎨 Building placeholder token output..."
|
||||
mkdir -p src/theme/generated
|
||||
mkdir -p tokens/export
|
||||
# Style Dictionary needs at least one source file to build
|
||||
if [ ! -f "tokens/primitives/colours.json" ]; then
|
||||
echo '{}' > tokens/primitives/colours.json
|
||||
echo '{}' > tokens/primitives/typography.json
|
||||
echo '{}' > tokens/primitives/spacing.json
|
||||
echo '{}' > tokens/primitives/effects.json
|
||||
echo '{}' > tokens/semantic/colours.json
|
||||
echo '{}' > tokens/semantic/typography.json
|
||||
echo '{}' > tokens/semantic/spacing.json
|
||||
echo " Created empty token placeholder files."
|
||||
fi
|
||||
|
||||
# ─── 4. TypeScript check ─────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "🔍 Running TypeScript check..."
|
||||
npx tsc --noEmit 2>/dev/null && echo " ✓ TypeScript OK" || echo " ⚠ TypeScript errors (expected before tokens are created)"
|
||||
|
||||
# ─── 5. Git init ─────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ ! -d ".git" ]; then
|
||||
echo "🔧 Initialising git repository..."
|
||||
git init
|
||||
git add -A
|
||||
git commit -m "Initial scaffold: FA Design System project structure"
|
||||
echo " ✓ Git initialised with initial commit"
|
||||
else
|
||||
echo "🔧 Git already initialised."
|
||||
fi
|
||||
|
||||
# ─── 6. Summary ──────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "╔══════════════════════════════════════════════╗"
|
||||
echo "║ ✓ Bootstrap complete! ║"
|
||||
echo "╚══════════════════════════════════════════════╝"
|
||||
echo ""
|
||||
echo "Next steps:"
|
||||
echo ""
|
||||
echo " 1. SET UP FIGMA MCP:"
|
||||
echo " claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp"
|
||||
echo " Then restart Claude Code and authenticate via /mcp"
|
||||
echo ""
|
||||
echo " 2. START CLAUDE CODE:"
|
||||
echo " cd $(pwd)"
|
||||
echo " claude"
|
||||
echo ""
|
||||
echo " 3. CHECK STATUS:"
|
||||
echo " /status"
|
||||
echo ""
|
||||
echo " 4. CREATE YOUR TOKENS:"
|
||||
echo " /create-tokens [provide brand colours, fonts, and context]"
|
||||
echo ""
|
||||
echo " 5. START STORYBOOK (in a separate terminal):"
|
||||
echo " npm run storybook"
|
||||
echo ""
|
||||
echo " 6. BUILD YOUR FIRST ATOM:"
|
||||
echo " /build-atom Button"
|
||||
echo ""
|
||||
BIN
brandassets/images/heroes/hero-3.png
Normal file
BIN
brandassets/images/heroes/hero-3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 MiB |
File diff suppressed because it is too large
Load Diff
@@ -1,110 +0,0 @@
|
||||
# Component registry
|
||||
|
||||
Tracks the status, specification, and key details of every component in the
|
||||
design system. Agents MUST check this before building a component (to avoid
|
||||
duplicates) and MUST update it after completing one.
|
||||
|
||||
## Status definitions
|
||||
|
||||
- **planned**: Component is identified but not yet started
|
||||
- **in-progress**: Component is being built
|
||||
- **review**: Component is built, awaiting human review
|
||||
- **done**: Component is reviewed and approved
|
||||
- **needs-revision**: Component needs changes based on review feedback
|
||||
|
||||
## Atoms
|
||||
|
||||
| Component | Status | Variants | Tokens used | Notes |
|
||||
|-----------|--------|----------|-------------|-------|
|
||||
| Button | done | contained, soft, outlined, text × xs, small, medium, large × primary, secondary + loading, underline, fullWidth | button.height/paddingX/paddingY/fontSize/iconSize/iconGap/borderRadius, color.interactive.*, color.brand.100-300, color.neutral.200-700 | Primary interactive element. Merges Text Button from Figma. Soft variant = Figma's Secondary/Brand & Secondary/Grey. |
|
||||
| IconButton | done | default, primary, secondary, error × small, medium, large | Reuses button.height/iconSize tokens, color.interactive.*, color.neutral.* | Icon-only button (close, menu, actions). Wraps MUI IconButton. Rounded rect, brand hover, focus ring. |
|
||||
| Typography | done | displayHero, display1-3, displaySm, h1-h6, bodyLg, body1, body2, bodyXs, labelLg, label, labelSm, caption, captionSm, overline, overlineSm + maxLines, gutterBottom | typography.* (all semantic typography tokens), fontFamily.body, fontFamily.display | Text display system. Thin MUI wrapper with maxLines truncation. |
|
||||
| Input | done | medium, small × default, hover, focus, error, success, disabled + startIcon, endIcon, required, multiline | input.height/paddingX/paddingY/fontSize/borderRadius/gap/iconSize, color.neutral.300-400, color.brand.500, color.feedback.error/success, color.text.secondary | External label pattern, branded focus ring, two sizes aligned with Button. Adds startIcon/endIcon and success state beyond Figma. |
|
||||
| Badge | done | soft, filled × default, brand, success, warning, error, info × small, medium + icon | badge.height/paddingX/fontSize/iconSize/iconGap/borderRadius, color.feedback.*, color.brand.200/700 | Status indicator pill. Soft (tonal) or filled (solid). 6 colours, 2 sizes, optional leading icon. |
|
||||
| Icon | planned | various sizes | | Icon wrapper component |
|
||||
| Avatar | planned | image, initials, icon × small, medium, large | | User/entity representation |
|
||||
| Divider | done | horizontal, vertical × fullWidth, inset, middle + text, flexItem | color.border.default (via palette.divider) | Visual separator. Wraps MUI Divider. Supports text children and orientation. |
|
||||
| Chip | done | filled, outlined × small, medium × clickable, deletable, selected × default, primary | chip.height/paddingX/fontSize/iconSize/deleteIconSize/iconGap/borderRadius, color.neutral.200-700, color.brand.200-700 | Interactive tag. Wraps MUI Chip with FA tokens. Selected state promotes to brand colour. Filled uses soft tonal bg (like Badge). |
|
||||
| Card | done | elevated, outlined × default, compact, none padding × interactive × selected | card.borderRadius/padding/shadow/border/background, color.surface.raised/subtle/warm, color.border.default/brand, shadow.md/lg | Content container. Elevated (shadow) or outlined (border). Interactive adds hover bg fill + shadow lift. Selected adds brand border + warm bg. Three padding presets. |
|
||||
| Switch | done | bordered style × checked, unchecked, disabled | switch.track.width/height/borderRadius, switch.thumb.size, color.interactive.*, color.neutral.400 | Toggle for add-ons/options. Wraps MUI Switch. Bordered pill, brand.500 fill when active. From Parsons 1.0 Figma Style One. |
|
||||
| Radio | done | checked, unchecked, disabled | radio.size/dotSize, color.interactive.*, color.neutral.400 | Single-select option. Wraps MUI Radio. Brand.500 fill when selected. From Parsons 1.0 Figma. |
|
||||
| ColourToggle | planned | inactive, hover, active, locked × single, two-colour × desktop, mobile | | Circular colour swatch picker for products. Custom component. Deferred until product detail organisms. |
|
||||
| Slider | planned | single, range × desktop, mobile | | Price range filter. Wraps MUI Slider. Deferred until search/filtering molecules. |
|
||||
| Link | done | underline: hover/always/none × any MUI colour | color.text.brand (copper brand.600, 4.8:1), color.interactive.active | Navigation text link. Wraps MUI Link. Copper default, underline on hover, focus ring. |
|
||||
| Collapse | done | in/out × unmountOnExit | (none — uses MUI defaults) | Progressive disclosure wrapper. Thin MUI Collapse wrapper with unmountOnExit default. Slide-down animation for wizard field reveal. |
|
||||
| DialogShell | done | open/closed × with/without back button × with/without footer | (theme defaults — borderRadius, palette) | Standard dialog container. Header (title + optional back + close), divider, scrollable body, optional footer. Used by FilterPanel and ArrangementDialog. |
|
||||
| ToggleButtonGroup | done | exclusive single-select × small, medium, large × error × fullWidth + descriptions | color.neutral.100-200, color.brand.50/100, color.interactive.focus, color.feedback.error | Button-select for binary/small-set choices. Fieldset/legend a11y, external label, helper/error text. Brand styling on selected. |
|
||||
|
||||
## Molecules
|
||||
|
||||
| Component | Status | Composed of | Notes |
|
||||
|-----------|--------|-------------|-------|
|
||||
| FormField | planned | Input + Typography (label) + Typography (helper) | Standard form field with label and validation |
|
||||
| ProviderCard | done | Card + Typography + Badge + Tooltip | Provider listing card. Verified: image + logo (64px rounded rect) + "Verified" badge. Unverified: text-only with top accent bar. Capability badges with info icon + tooltip. Price split typography. No footer. 4 component tokens. |
|
||||
| VenueCard | done | Card + Typography | Venue listing card. Always has photo + location + capacity ("X guests") + price ("From $X"). No verification tiers, no logo, no badges. 3 component tokens. Critique: 33/40. |
|
||||
| MapCard | planned | Card + Typography + Badge | Compact horizontal map popup card. Deferred until map integration. |
|
||||
| ServiceOption | done | Card (interactive, selected) + Typography | Selectable service option for arrangement flow. Heading + optional price (right-aligned) + optional description. role="radio" + aria-checked. Disabled state with opacity token. Maps to Figma ListItemPurchaseOption. |
|
||||
| SearchBar | done | Input + IconButton + Button | Search input with optional submit button. Enter-to-submit, progressive clear button, inline loading spinner. Guards empty submissions, refocuses after clear. role="search" landmark. Critique: 35/40. |
|
||||
| AddOnOption | done | Card (interactive, selected) + Typography + Switch | Toggleable add-on for arrangement flow extras. Heading + optional price + description + Switch. Click-anywhere toggle. Maps to Figma ListItemAddItem (2350:40658). |
|
||||
| StepIndicator | done | Typography + Box | Horizontal segmented progress bar. Brand gold for completed/current steps, grey for upcoming. Responsive bar height (10px/6px). Maps to Figma Progress Bar - Steps (2375:47468). |
|
||||
| LineItem | done | Typography + Tooltip + InfoOutlinedIcon | Name + optional info tooltip + optional price. Supports allowance asterisk, total variant (bold + top border). Font weight 500 (D019), prices text.secondary for readability hierarchy. Audit: 19/20. |
|
||||
| ProviderCardCompact | done | Card (outlined) + Typography | Horizontal compact provider card — image left, name + location + rating right. Used at top of Package Select page. Separate from vertical ProviderCard. |
|
||||
| CartButton | done | Button + DialogShell + LineItem + Divider + Typography | Outlined pill trigger: receipt icon + "Your Plan" + formatted total in brand colour. Click opens DialogShell with items grouped by section via LineItem, total row. Mobile: icon + price only. Lives in WizardLayout `runningTotal` slot. |
|
||||
|
||||
## Organisms
|
||||
|
||||
| Component | Status | Composed of | Notes |
|
||||
|-----------|--------|-------------|-------|
|
||||
| ServiceSelector | done | ServiceOption × n + Typography + Button | Single-select service panel for arrangement flow. Heading + subheading + ServiceOption list (radiogroup) + optional continue Button. Manages selection state via selectedId/onSelect. maxDescriptionLines pass-through. |
|
||||
| PricingTable | planned | PriceCard × n + Typography | Comparative pricing display |
|
||||
| PackageDetail | done | LineItem × n + Typography + Button + Divider | Right-side package detail panel. Warm header band (surface.warm) with "Package" overline, name, price (brand colour), Make Arrangement + Compare (with loading) buttons. Sections (before total) + total + extras (after total, with subtext). T&C grey footer. Audit: 19/20. Maps to Figma Package Select (5405:181955). |
|
||||
| FuneralFinder (V3) | done | Typography + Button + Divider + Select + MenuItem + OutlinedInput + custom StatusCard/SectionLabel | **Production version.** Hero search widget — clean form with status cards. Standard card container (surface.raised, card shadow). "How Can We Help" section: two side-by-side StatusCards (Immediate Need default-selected / Pre-planning) — white bg, neutral border, brand border + warm bg when selected, stack on mobile. "Funeral Type" Select + "Location" OutlinedInput with pin icon — standard outlined fields, no focus ring (per design). Overline section labels (text.secondary). CTA "Find Funeral Directors →" always active — validates on click, scrolls to first missing field. Required: status + location. Funeral type defaults to "show all". Dividers after header and before CTA. WAI-ARIA roving tabindex on radiogroup. aria-labelledby via useId(). Critique: 33/40 (Good). Audit: 18/20 (Excellent). |
|
||||
| FuneralFinder V1 | archived | Typography + Button + Chip + Input + Divider + Link + custom ChoiceCard/TypeCard/CompletedRow/StepHeading | Archived — viewable in Storybook under Archive/. Stepped conversational flow. Audit: 14/20. Critique: 29/40. |
|
||||
| FuneralFinder V2 | archived | Typography + Button + Input + Divider + Select + MenuItem + custom StepCircle | Archived — viewable in Storybook under Archive/. Quick-form with step circles. Audit: 18/20. Critique: 33/40. |
|
||||
| ArrangementForm | planned | StepIndicator + ServiceSelector + AddOnOption + Button + Typography | Multi-step arrangement wizard. Deferred — build remaining atoms/molecules first. |
|
||||
| Navigation | done | AppBar + Link + IconButton + Button + Divider + Drawer | Responsive site header. Desktop: logo left, links right, optional CTA. Mobile: hamburger + drawer with nav items, CTA, help footer. Sticky, grey surface bg (surface.subtle). Real FA logo from brandassets/. Maps to Figma Main Nav (14:108) + Mobile Header (2391:41508). |
|
||||
| Footer | done | Link × n + Typography + Divider + Container + Grid | Dark espresso (brand.950) site footer. Logo + tagline + contact (phone/email) + link group columns + legal bar. Semantic HTML (footer, nav, ul). Critique: 38/40 (Excellent). |
|
||||
|
||||
## Templates
|
||||
|
||||
| Component | Status | Composed of | Notes |
|
||||
|-----------|--------|-------------|-------|
|
||||
| WizardLayout | done | Container + Box + Link + Typography + Navigation (slot) + StepIndicator (slot) | Page-level layout for arrangement wizard. 5 variants: centered-form, list-map, list-detail, grid-sidebar, detail-toggles. Nav slot, sticky help bar, optional back link, optional progress stepper + running total. `<main>` landmark wrapper. |
|
||||
|
||||
## Pages
|
||||
|
||||
| Component | Status | Composed of | Notes |
|
||||
|-----------|--------|-------------|-------|
|
||||
| IntroStep | done | WizardLayout (centered-form) + ToggleButtonGroup × 2 + Collapse + Typography + Button + Divider | Wizard step 1 — entry point. forWhom (Myself/Someone else) + hasPassedAway (Yes/No) with progressive disclosure. Auto-sets hasPassedAway="no" for "Myself". `<form>` wrapper, aria-live subheading, grief-sensitive copy. Pure presentation. Audit: 18/20 → 20/20 after fixes. |
|
||||
| ProvidersStep | done | WizardLayout (list-map) + ProviderCard + SearchBar + Chip + Typography + Button | Wizard step 2 — provider selection. List-map split: provider cards w/ radiogroup + search + filter chips (left), map slot (right). aria-live results count, back link. ProviderCard extended with HTML/ARIA passthrough. Audit: 18/20. |
|
||||
| PackagesStep | done | WizardLayout (list-detail) + ProviderCardCompact + ServiceOption + PackageDetail + Badge + TextField + Typography + Button | Wizard step 3 — package selection. List-detail split: compact provider + budget filter + package list w/ radiogroup (left), PackageDetail breakdown (right). "Most Popular" badge. Mobile Continue button. |
|
||||
| ~~PreviewStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). Package preview + "what's next" checklist now in the dialog's preview step. |
|
||||
| ~~AuthGateStep~~ | removed | — | Replaced by ArrangementDialog organism (D-E). SSO/email auth flow now in the dialog's auth step. |
|
||||
| DateTimeStep | done | WizardLayout (centered-form) + Input + TextField (date) + RadioGroup + Collapse + Divider + Button + Link | Wizard step 6 — details & scheduling. Deceased name (Input atom, external label) + preferred dates (up to 3, progressive disclosure) + time-of-day radios. Service tradition removed (flows from provider/package). Dividers between sections. Grief-sensitive labels. Save-and-exit CTA. |
|
||||
| VenueStep | done | WizardLayout (centered-form) + VenueCard + AddOnOption + Collapse + Chip + TextField + Divider + Button | Wizard step 7a — venue browsing. Click-to-navigate card grid with search/filters. Leads to VenueDetailStep. |
|
||||
| VenueDetailStep | done | WizardLayout (detail-toggles) + ImageGallery + Card + Chip + Typography + Button + Divider | Wizard step 7b — venue detail. Two-panel: gallery/description/features/location (left), name/meta/price/CTA/religions (right). Informational service preview. |
|
||||
| VenueServicesStep | done | WizardLayout (centered-form) + AddOnOption + Card + Typography + Button + Divider | Wizard step 7c — venue services. Compact venue card, availability notices, AddOnOption toggles with "View more" for long descriptions. Follows VenueDetailStep. |
|
||||
| CrematoriumStep | done | WizardLayout (centered-form) + Card + Badge + ToggleButtonGroup + Typography + Button + Divider | Wizard step 8 — crematorium. Two variants: Service & Cremation (compact card + witness Yes/No toggle), Cremation Only (compact card + "Cremation Only" badge + "Included in Package" notice). Single pre-selected crematorium, no multi-select. |
|
||||
| CemeteryStep | done | WizardLayout (centered-form) + ToggleButtonGroup + Collapse + TextField (select) + Typography + Button + Divider | Wizard step 9 — cemetery. ToggleButtonGroups (Yes/No/Not sure) with progressive disclosure. Own plot → locate dropdown. No plot → preference? → select dropdown. No card grid. |
|
||||
| CoffinsStep | done | WizardLayout (grid-sidebar) + Card + Badge + Collapse + Slider + TextField + Pagination + Divider + Link | Wizard step 10 — coffin browsing. Grid-sidebar: filter sidebar (categories with expandable subcategories, dual-knob price slider with editable inputs, sort by) + 3-col card grid. CoffinCard with thumbnail hover preview. Equal-height cards, subtle bg for white-bg product photos. Card click → CoffinDetailsStep (no Continue). 20/page max. Conditional allowance info bubble. |
|
||||
| CoffinDetailsStep | done | WizardLayout (detail-toggles) + ImageGallery + Divider + Button | Wizard step 11 — coffin detail. Two-panel: gallery + product details dl (left), name + description + colour swatches + allowance-aware price + CTA (right). Allowance logic: fully covered / partially covered / no allowance. Colour selection does not affect price. |
|
||||
| ~~AdditionalServicesStep~~ | removed | — | Replaced by IncludedServicesStep + ExtrasStep. Split for clearer distinction between free inclusions and paid extras. |
|
||||
| IncludedServicesStep | done | WizardLayout (centered-form) + AddOnOption + RadioGroup + Collapse + Divider + Button | Wizard step 12a — included services. Package inclusions at no additional cost: dressing, viewing (with same-venue sub-option), prayers/vigil, funeral announcement. Sub-options render inside parent card. |
|
||||
| ExtrasStep | done | WizardLayout (centered-form) + AddOnOption + Card + Switch + RadioGroup + Collapse + Divider + Button | Wizard step 12b — optional extras. Lead-gen interest capture: catering, music (inline live musician toggle + musician type), coffin bearing (toggle + bearer type), newspaper notice. POA via `priceLabel`. Tally of priced selections. No nested cards. |
|
||||
| SummaryStep | done | WizardLayout (centered-form) + Card + Paper + DialogShell + Button + Link + Divider | Wizard step 13 — plan review. Visual cart layout: arrangement details (2-col grid), compact cards with thumbnails for provider/venue/crematorium/coffin, checklist for included services, priced list for extras. Allowance display (fully covered vs remaining). Share dialog (multi-email). Location pin icons. Full-width CTA. |
|
||||
| PaymentStep | done | WizardLayout (centered-form) + ToggleButtonGroup + Paper + Collapse + Checkbox + Divider + Button | Wizard step 14 — payment. Plan (full/deposit) + method (card/bank). PayWay iframe slot. Bank transfer details. Terms checkbox. |
|
||||
| ConfirmationStep | done | WizardLayout (centered-form) + Button | Wizard step 15 — confirmation. Terminal page. At-need: "submitted" + callback. Pre-planning: "saved" + return-anytime. Muted success icon. |
|
||||
| HomePage | done | FuneralFinderV3 + Card + Button + Typography + Accordion + Navigation (prop) + Footer (prop) | Marketing landing page. 7 sections: hero (display3 serif), FuneralFinder widget (overlapping card), partner logos carousel (CSS-only infinite scroll), 4 feature cards (outlined, compact, circular icon bg), reviews/testimonials (dark bg, Google aggregate), CTA banner (displaySm serif), FAQ accordion. Serif reserved for hero + CTA. Critique: 32/40, Audit: 16/20. First pass — will iterate. |
|
||||
|
||||
## Future enhancements
|
||||
|
||||
Deferred items that should be addressed when the relevant components or patterns
|
||||
are needed. Check this section before building new components — an item here may
|
||||
be relevant to your current work.
|
||||
|
||||
| Item | Relates to | Trigger | Notes |
|
||||
|------|-----------|---------|-------|
|
||||
| Destructive button colours | Button | When building delete/cancel flows | `color="error"` already works via MUI palette. May need `soft` variant styling for error/warning/success colours. |
|
||||
| Link-as-button | Button | When building Navigation or link-heavy pages | Use MUI's `component="a"` or `href` prop. May warrant a separate Link atom or a `Button` story showing the pattern. |
|
||||
| ~~IconButton atom~~ | ~~IconButton~~ | ~~Resolved~~ | ~~Built as atom. Rounded rect, 3 sizes, 4 colours, focus ring.~~ |
|
||||
| ~~Google Fonts loading~~ | ~~Typography~~ | ~~Resolved~~ | ~~Added to .storybook/preview-head.html and index.html~~ |
|
||||
@@ -1,295 +0,0 @@
|
||||
# Design decisions log
|
||||
|
||||
Every design decision that affects tokens, components, or system architecture
|
||||
MUST be logged here with rationale. This ensures consistency across sessions
|
||||
and agents. Agents MUST check this file before making any decision that could
|
||||
contradict a previous one.
|
||||
|
||||
## Format
|
||||
|
||||
```
|
||||
### [Decision ID] — [Short title]
|
||||
**Date:** [date]
|
||||
**Category:** token | component | architecture | convention
|
||||
**Decision:** [What was decided]
|
||||
**Rationale:** [Why this decision was made]
|
||||
**Affects:** [Which tokens/components/files this impacts]
|
||||
**Alternatives considered:** [What else was considered and why it was rejected]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Decisions
|
||||
|
||||
### D001 — Primary brand colour is warm gold #BA834E
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Primary brand colour (color.brand.500) is #BA834E — warm gold.
|
||||
**Rationale:** Extracted from Parsons brand swatches. Matches the existing FA 1.0 CTA button colour ("Add to package"). Warm gold conveys trust and professionalism appropriate for funeral services. The colour has a 3.7:1 contrast ratio on white, suitable for large text and interactive elements (buttons with white text).
|
||||
**Affects:** color.brand.500, color.interactive.default, MUI palette.primary.main
|
||||
**Alternatives considered:** #B0610F (copper) was considered for primary but it's darker and more aggressive. Placed at brand.600 for hover/emphasis instead.
|
||||
|
||||
### D002 — Primary text colour is charcoal #2C2E35 (not pure black)
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Primary text colour is #2C2E35 (color.neutral.800) — charcoal with cool blue undertone.
|
||||
**Rationale:** From brand swatch. Softer than pure black for extended reading comfort while maintaining 13.2:1 contrast ratio on white (well exceeds WCAG AA). The cool undertone complements the sage secondary palette.
|
||||
**Affects:** color.text.primary, MUI palette.text.primary
|
||||
**Alternatives considered:** Pure black (#000000) — higher contrast but causes eye fatigue on white backgrounds. Added as color.black for rare use.
|
||||
|
||||
### D003 — Brand link/accent colour uses copper #B0610F
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Brand-coloured text (links, inline emphasis) uses #B0610F (color.brand.600 / copper) not the base gold.
|
||||
**Rationale:** The base gold #BA834E has only 3.7:1 contrast on white which doesn't meet WCAG AA for normal text (4.5:1 required). The copper #B0610F provides 4.8:1 contrast, meeting AA. It's also visually more assertive for link text.
|
||||
**Affects:** color.text.brand, color.interactive.hover
|
||||
**Alternatives considered:** brand.700 (#8B4E0D) at 6.7:1 — meets AAA but is too dark and loses the warm "brand" feel.
|
||||
|
||||
### D004 — Sage palette as secondary colour family
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Secondary palette uses cool sage/slate tones (#4C5B6B as sage.700) rather than a second warm colour.
|
||||
**Rationale:** From brand swatches (cool row). The sage provides visual contrast against the warm brand palette and adds a calming, professional quality appropriate for the funeral services context. Used for secondary buttons and less prominent actions.
|
||||
**Affects:** color.sage.*, color.interactive.secondary, MUI palette.secondary
|
||||
**Alternatives considered:** Using a lighter warm tone for secondary — rejected as it wouldn't provide enough visual distinction from primary.
|
||||
|
||||
### D005 — Display font is Noto Serif SC for H1-H2 headings
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Display/heading font is Noto Serif SC for Display, H1, and H2 levels. H3+ use Montserrat (body font).
|
||||
**Rationale:** User specified Noto Serif SC as display font and Montserrat as primary. The serif at H1-H2 adds warmth and gravitas suitable for funeral services. H3+ switches to Montserrat to create a clear hierarchy break.
|
||||
**Affects:** fontFamily.display, fontFamily.body, typography.display/h1/h2/h3/h4
|
||||
**Alternatives considered:** Using serif for all headings — rejected as it would make H3/H4 feel too formal at smaller sizes.
|
||||
|
||||
### D006 — Warning text uses amber.700 for AA compliance
|
||||
**Date:** 2025-03-25
|
||||
**Category:** token
|
||||
**Decision:** Warning text colour is amber.700 (#A36B00) not amber.600 (#CC8500).
|
||||
**Rationale:** amber.600 (3.6:1 contrast on white) only passes AA for large text. For warning text that must be readable at body size, amber.700 at 5.1:1 meets WCAG AA for normal text.
|
||||
**Affects:** color.text.warning
|
||||
**Alternatives considered:** amber.800 — passes AAA but is too dark to read as "amber/warning".
|
||||
|
||||
### D007 — Style Dictionary requires usesDtcg: true
|
||||
**Date:** 2025-03-25
|
||||
**Category:** architecture
|
||||
**Decision:** Added `usesDtcg: true` to Style Dictionary v4.4.0 config.
|
||||
**Rationale:** Without this flag, SD v4 doesn't properly populate `allTokens` for the CSS `css/variables` format, resulting in empty CSS output. JS and JSON formats work without it but CSS requires it for DTCG token format.
|
||||
**Affects:** style-dictionary/config.js, CSS output
|
||||
**Alternatives considered:** Custom format — rejected as the built-in format works correctly with the flag.
|
||||
|
||||
### D008 — Responsive typography via @media in MUI theme
|
||||
**Date:** 2025-03-25
|
||||
**Category:** architecture
|
||||
**Decision:** Mobile font sizes are handled via `@media (max-width:600px)` queries in the MUI theme typography config, not via `responsiveFontSizes()`.
|
||||
**Rationale:** Explicit media queries allow precise control over mobile sizes (28px, 24px, 20px, 18px) matching the token definitions. MUI's `responsiveFontSizes()` uses its own scaling algorithm that can't be configured to match our exact values.
|
||||
**Affects:** src/theme/index.ts, typography.*.fontSizeMobile tokens
|
||||
**Alternatives considered:** `responsiveFontSizes()` utility — rejected due to lack of precise size control.
|
||||
|
||||
### D009 — Merge Text Button into Button atom
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Text Button is not a separate atom. It is implemented as `<Button variant="text">` with an optional `underline` prop for link-style appearance.
|
||||
**Rationale:** In MUI, text buttons are a variant of Button (`variant="text"`), not a separate component. Having both Button and TextButton creates API confusion. The Figma "ghost" variant and "text button" both map to MUI's text variant. The `underline` prop handles the design's "default.underline" state.
|
||||
**Affects:** Button component API, Storybook story structure
|
||||
**Alternatives considered:** Separate TextButton component — rejected as it duplicates Button logic and creates ambiguity for developers.
|
||||
|
||||
### D010 — Button sizes adjusted for touch targets
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Button sizes are xs: 28px, sm: 32px, md: 40px, lg: 48px — all multiples of 4px. Large size (48px) meets the 44px minimum touch target for mobile.
|
||||
**Rationale:** Original Figma sizes (sm: 26px, md: 37px) don't meet the 44px minimum touch target specified in the design system conventions. Adjusted to a clean 4px-grid scale that includes a large size suitable for mobile CTAs. FA's audience may be older or in emotional distress — generous touch targets are important.
|
||||
**Affects:** tokens/component/button.json, MUI theme MuiButton overrides
|
||||
**Alternatives considered:** Keeping original Figma sizes — rejected because neither met the 44px touch target minimum.
|
||||
|
||||
### D011 — Use @mui/icons-material over Font Awesome
|
||||
**Date:** 2026-03-25
|
||||
**Category:** architecture
|
||||
**Decision:** Primary icon source is `@mui/icons-material`. Custom SVGs wrapped in MUI's `SvgIcon` for any FA-specific icons.
|
||||
**Rationale:** MUI icons extend `SvgIcon`, so they work natively with MUI Button's `startIcon`/`endIcon`, inherit theme colours, and are tree-shakeable. Font Awesome requires an adapter layer to work with MUI's icon API. All standard UI icons from the Figma designs exist in the MUI icon set.
|
||||
**Affects:** Icon approach, Button icon integration, dependencies
|
||||
**Alternatives considered:** `@fortawesome/react-fontawesome` — rejected due to adapter complexity and extra dependency. User was using FA icons in Figma but agreed MUI icons are more pragmatic for the code implementation.
|
||||
|
||||
### D012 — Button responsive sizing is a composition concern
|
||||
**Date:** 2026-03-25
|
||||
**Category:** architecture
|
||||
**Decision:** Button size is an explicit prop (`size="xs" | "small" | "medium" | "large"`), not responsive by default. Layout components choose the appropriate size per breakpoint.
|
||||
**Rationale:** Keeps the Button component simple and predictable. Responsive behaviour is a layout decision — a card might use `size="large"` on mobile for touch targets and `size="medium"` on desktop. This avoids the complexity of Figma variable modes for mobile/desktop scaling inside components.
|
||||
**Affects:** Button component API, consumer usage patterns
|
||||
**Alternatives considered:** Figma variable modes for responsive scaling within the component — rejected as over-engineered. MUI's `useMediaQuery` or responsive `sx` props handle this at the composition level.
|
||||
|
||||
### D013 — Soft variant for tonal/muted buttons
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Added `variant="soft"` custom MUI Button variant for tonal/muted fill buttons. Maps to Figma's "Secondary/Brand" and "Secondary/Grey" columns.
|
||||
**Rationale:** Figma review revealed that "Secondary/Brand" (bg #e4cdb3, text #845830) and "Secondary/Grey" (bg #dcdde0, text #434b52) are NOT outlined buttons — they are filled buttons with softer, tonal colours. This pattern is common in modern design systems (Material Design 3 calls it "tonal", Radix calls it "soft"). Primary soft uses brand.200 bg + brand.700 text. Secondary soft uses neutral.200 bg + neutral.700 text.
|
||||
**Affects:** MUI theme MuiButton variants, Button component API, Storybook stories
|
||||
**Alternatives considered:** Separate color options for each Figma column — rejected as it doesn't map to MUI's color+variant model. The soft variant is more extensible — any future color automatically gets a soft treatment.
|
||||
|
||||
### D014 — Secondary palette changed from sage to neutral grey
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** MUI secondary palette changed from sage (#4C5B6B sage.700) to neutral grey (#525252 neutral.600). Sage remains available in the token system for surfaces but is no longer the secondary button colour.
|
||||
**Rationale:** User feedback that secondary buttons should match Figma's "Secondary/Grey" which uses neutral tones, not the blue-green sage. The sage palette is still valuable for cool surfaces (color.surface.cool) but shouldn't be the primary secondary interactive colour.
|
||||
**Affects:** MUI palette.secondary, all MuiButton secondary overrides (outlined, text, soft)
|
||||
**Alternatives considered:** Adding a third "neutral" custom colour and keeping sage as secondary — rejected as unnecessarily complex when no other components use the sage secondary yet.
|
||||
|
||||
### D015 — Loading spinner positioned on the right
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Button loading spinner (CircularProgress) appears after the label text, on the right side of the button.
|
||||
**Rationale:** User preference. Right-side positioning follows the convention of trailing indicators (like endIcon) and feels more natural for async state feedback.
|
||||
**Affects:** Button component render order
|
||||
**Alternatives considered:** Left-side spinner (before text) — rejected per user feedback.
|
||||
|
||||
### D016 — Primary button colour confirmed as copper (brand.600)
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** Primary button fill (palette.primary.main) stays as brand.600 copper (#B0610F), not brand.500 warm gold (#BA834E). User confirmed.
|
||||
**Rationale:** Brand.600 provides 4.8:1 contrast ratio with white text, meeting WCAG AA for all text sizes. Brand.500 only achieves 3.7:1 (AA for large text only). Since button labels can vary in size, the stronger contrast is preferred. The visual warmth is preserved through the soft variant (brand.200 bg) and hover states.
|
||||
**Affects:** palette.primary.main, all contained primary buttons, interactive token
|
||||
**Alternatives considered:** Brand.500 warm gold — rejected as it doesn't meet AA for normal-sized text.
|
||||
|
||||
### D017 — Headings use Montserrat Bold, not Noto Serif SC
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** All headings (h1-h6) use Montserrat Bold (700), not Noto Serif SC. Noto Serif SC is reserved exclusively for display variants (displayHero through displaySm).
|
||||
**Rationale:** User's FA 2.0 Figma design separates display (serif, for marketing/hero) from headings (sans-serif, for content structure). This is cleaner than the previous approach where h1-h2 used serif. Montserrat Bold provides strong hierarchy without the formality of serif at content-level sizes.
|
||||
**Affects:** MUI theme h1-h6 fontFamily, semantic typography tokens. Supersedes D005.
|
||||
**Alternatives considered:** Keep D005 (serif for h1-h2) — rejected per user's updated Figma design.
|
||||
|
||||
### D018 — Display weight is Regular (400), not Bold
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** Display text (Noto Serif SC) uses Regular weight (400) across all 5 display levels, not Bold (700).
|
||||
**Rationale:** Serif fonts carry inherent visual weight. At 40-80px, Regular Noto Serif SC has enormous presence. Bold at these sizes would be overwhelming and less elegant.
|
||||
**Affects:** typography.displayHero/1/2/3/Sm fontWeight tokens
|
||||
**Alternatives considered:** Bold (700) — the previous implementation. Rejected per user's Figma which uses Regular.
|
||||
|
||||
### D019 — Body text weight is Medium (500)
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** Body text uses Montserrat Medium (500) instead of Regular (400). User found Regular too light for comfortable reading.
|
||||
**Rationale:** User feedback that Montserrat Regular is "just a little bit too light." The Figma specified weight 450 which isn't available in Google Fonts, so 500 (Medium) is the closest available weight that provides the desired visual density.
|
||||
**Affects:** typography.body/bodyLg/bodySm/bodyXs fontWeight tokens
|
||||
**Alternatives considered:** 400 Regular — too light per user. 450 — not available in Google Fonts Montserrat.
|
||||
|
||||
### D020 — Expanded type scale: 21 variants across 6 categories
|
||||
**Date:** 2026-03-25
|
||||
**Category:** token
|
||||
**Decision:** Typography system expanded from ~12 to 21 variants organized in 6 categories: Display (5 levels), Heading (6 levels), Body (4 sizes), Label (3 sizes), Caption (2 sizes), Overline (2 sizes). Caption/sm and overline/sm floored at 11px for accessibility.
|
||||
**Affects:** primitive typography tokens, semantic typography tokens, MUI theme, Typography component
|
||||
**Alternatives considered:** Keeping the simpler scale — rejected as user explicitly wanted more variety matching their FA 2.0 Figma type system.
|
||||
|
||||
### D021 — Input uses external label pattern, not MUI floating label
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Input label sits above the input field as a separate element, not as MUI's floating/outlined label that sits on the border. Uses MUI's `InputLabel` with `position: static` and `shrink: true`.
|
||||
**Rationale:** The Figma design shows the label as a distinct element above the input with a 6px gap. The floating label pattern is harder to read and interact with, especially for FA's audience who may be elderly or in distress. External labels have better usability research support for form comprehension.
|
||||
**Affects:** Input component structure, MUI InputLabel styling, no notch in OutlinedInput
|
||||
**Alternatives considered:** MUI's default floating label (outlined variant) — rejected as it doesn't match the Figma design and has usability concerns for the target audience.
|
||||
|
||||
### D022 — Input sizes: medium (48px) and small (40px)
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Input has two sizes: medium (48px, default) and small (40px). Medium aligns with Button large (48px) for search bar pairing. Small aligns with Button medium (40px) for compact layouts.
|
||||
**Rationale:** Figma designed inputs at 64px which is too tall relative to other components and creates misalignment with buttons. 48px meets the 44px touch target minimum and aligns with our Button large. Both sizes sit on the 4px grid.
|
||||
**Affects:** tokens/component/input.json, MUI theme MuiOutlinedInput size overrides
|
||||
**Alternatives considered:** Single size at 64px (Figma original) — rejected as it dwarfs adjacent buttons. Three sizes (sm/md/lg) — rejected as premature; two sizes cover all current form patterns.
|
||||
|
||||
### D023 — Input focus ring uses brand.500 warm gold, not brand.600 copper
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Input focus ring uses brand.500 (#BA834E warm gold) via a double box-shadow (2px white + 4px brand), not the brand.600 copper used for Button focus outlines.
|
||||
**Rationale:** The Figma FA 2.0 design explicitly specifies brand.500 for the input focus ring as an `elevation-special/focus-ring` effect token. The warm gold is softer and more appropriate for form inputs where the focus indicator should be visible but not aggressive. Buttons use brand.600 outline because they need to stand out against coloured backgrounds.
|
||||
**Affects:** MUI theme MuiOutlinedInput focus styles
|
||||
**Alternatives considered:** Using brand.600 (same as Button) — rejected as the Figma explicitly uses brand.500 for inputs.
|
||||
|
||||
### D024 — Input label stays neutral on error/success states
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Input label colour remains `text.secondary` regardless of error/success state. Only the border and helper text change colour.
|
||||
**Rationale:** Per the Figma design. A full-red label would feel alarming, which is inappropriate for FA's sensitive context. The subtle approach (border + helper text colour change) communicates the issue without distress. Users who are already in an emotional state don't need aggressive visual feedback.
|
||||
**Affects:** InputLabel Mui-error and Mui-focused style overrides
|
||||
**Alternatives considered:** Colouring the label red on error — rejected as too aggressive for the audience.
|
||||
|
||||
### D025 — Added startIcon/endIcon convenience props and success state
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Input component adds three features beyond the Figma design: (1) `startIcon` prop for leading icons, (2) `endIcon` prop for trailing icons (convenience wrapper around MUI adornments), (3) `success` boolean prop for validation success state (green border + green helper text). Error-state icons (ErrorOutline) and success-state icons (CheckCircleOutline) are recommended patterns in stories.
|
||||
**Rationale:** Leading icons (email, phone, dollar) are essential for FA's arrangement forms. Success state provides positive feedback after validation. The Figma only had trailing icon, but leading icons are a near-universal production need. The `startIcon`/`endIcon` props are simpler than MUI's `InputAdornment` pattern while remaining compatible with raw adornments via `startAdornment`/`endAdornment`.
|
||||
**Affects:** Input component API, InputAdornment usage, Storybook stories
|
||||
**Alternatives considered:** Only supporting MUI's raw adornment API — rejected as too verbose for the common case. The convenience props are ergonomic while the raw props remain available for complex cases (e.g., password toggle with IconButton).
|
||||
|
||||
### D026 — ProviderCard establishes the molecule pattern
|
||||
**Date:** 2026-03-25
|
||||
**Category:** architecture
|
||||
**Decision:** ProviderCard is the first molecule. Molecules compose existing atoms via `sx` props — no MUI theme overrides. All styling from token CSS variables and theme accessors.
|
||||
**Rationale:** Keeps molecules lightweight and composable. Theme overrides are reserved for atoms where global consistency matters. Molecules are higher-level compositions with more context-specific layouts.
|
||||
**Affects:** All future molecules (VenueCard, MapCard, ServiceOption, etc.)
|
||||
**Alternatives considered:** Adding MuiProviderCard theme overrides — rejected as molecules are page-specific compositions, not reusable primitives.
|
||||
|
||||
### D027 — ProviderCard image is a URL string, not a ReactNode
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** `imageUrl` prop accepts a string rendered as CSS `background-image` with `cover`. Logo is also a URL string rendered as `<img>`.
|
||||
**Rationale:** Simpler API for the common case (CDN URLs). CSS background-image handles aspect ratio cropping automatically. `<img>` for logo supports `alt` text natively for accessibility.
|
||||
**Affects:** ProviderCard, VenueCard, MapCard APIs
|
||||
**Alternatives considered:** ReactNode for image — rejected as over-flexible. A ReactNode slot would allow video/carousel but adds API complexity for a feature we don't need yet.
|
||||
|
||||
### D028 — Logo is 48px (not 75px from Figma)
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** Logo overlay is 48px diameter, not the 75px shown in Figma.
|
||||
**Rationale:** On a ~340-400px card in a list panel, a 75px logo dominates the content area. 48px provides clear brand visibility while leaving sufficient space for the provider name and meta row.
|
||||
**Affects:** providerCard.logo.size token
|
||||
**Alternatives considered:** 75px from Figma — too large for the card width. 64px — still large relative to content.
|
||||
|
||||
### D029 — Footer is built into ProviderCard, not a slot
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** The "Packages from $X >" footer is rendered via a `startingPrice` number prop, not a children/slot pattern.
|
||||
**Rationale:** The footer is structurally identical across all provider cards — warm background, "Packages from" text, price, chevron. Only the price value changes. A slot would add API complexity for no flexibility gain. If footer is omitted (no startingPrice), the bar is simply absent.
|
||||
**Affects:** ProviderCard API, VenueCard may use a similar pattern
|
||||
**Alternatives considered:** children/render prop for footer — rejected as over-engineered for a fixed layout.
|
||||
|
||||
### D030 — Verified is an explicit boolean, not derived from imageUrl
|
||||
**Date:** 2026-03-25
|
||||
**Category:** component
|
||||
**Decision:** `verified` boolean prop controls the visual treatment independently from `imageUrl`/`logoUrl`.
|
||||
**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.
|
||||
|
||||
### D032 — FuneralFinder V3 is the production version
|
||||
**Date:** 2026-03-27
|
||||
**Category:** component
|
||||
**Decision:** FuneralFinderV3 is the production widget. V1 and V2 are archived — their Storybook stories moved to `Archive/FuneralFinder V1` and `Archive/FuneralFinder V2`. Code remains in the repo for reference and client walkthroughs.
|
||||
**Rationale:** User chose V3 after reviewing all three. V3 scored highest on usability (clean form, no sequential lock, always-active CTA) while maintaining audit/critique scores (18/20, 33/40). V1 and V2 must remain viewable for client presentations showing the design evolution.
|
||||
**Affects:** FuneralFinder stories (title changes), component-registry (V3 = done, V1/V2 = archived), retroactive review (only V3 gets full treatment)
|
||||
**Alternatives considered:** Deleting V1/V2 — rejected because user needs them for client walkthroughs.
|
||||
|
||||
### D034 — Form error styling uses copper, not red
|
||||
**Date:** 2026-03-29
|
||||
**Category:** token
|
||||
**Decision:** Form error borders and helper text use copper (#B0610F / ColorTextBrand) instead of MUI's default red (#BC2F2F). `palette.error.main` remains red for non-form uses (destructive buttons, system alerts).
|
||||
**Rationale:** FA's grief-sensitive context makes aggressive red inappropriate for form validation. D024 established labels stay neutral on error; D034 extends this to borders and helper text for a fully warm error experience. Copper at 4.8:1 contrast on white meets WCAG AA.
|
||||
**Affects:** src/theme/index.ts (MuiOutlinedInput, MuiFormHelperText, MuiFormLabel overrides), ToggleButtonGroup atom, all page-level error Typography elements
|
||||
**Alternatives considered:** Changing `palette.error.main` globally to copper — rejected because destructive action buttons and system alerts may genuinely need red.
|
||||
|
||||
### D033 — Retroactive review uses P0/P1 only, interleaved with new work
|
||||
**Date:** 2026-03-27
|
||||
**Category:** architecture
|
||||
**Decision:** Existing components are reviewed using a condensed process (normalize → audit → fix P0/P1 only). Review runs as a proactive "morning housekeeping" pass at the start of each session (~30-60 min), then shifts to new component work.
|
||||
**Rationale:** P0/P1 are the issues that affect usability and accessibility. P2/P3 are cosmetic — not worth the risk of changing approved components. Interleaving ensures the foundation is solid before building on it, without dedicating entire sessions to review.
|
||||
**Affects:** Session workflow, CLAUDE.md startup procedure, docs/reference/retroactive-review-plan.md
|
||||
**Alternatives considered:** Dedicated review sessions — rejected as less efficient. Full P0-P3 fixes — rejected as too risky for approved components.
|
||||
@@ -1,800 +0,0 @@
|
||||
# Session log
|
||||
|
||||
This file tracks work completed across Claude Code sessions. Every agent MUST
|
||||
read this file before starting work and update it after completing work.
|
||||
|
||||
> Earlier sessions (2026-03-24 through 2026-03-26d) archived in
|
||||
> `docs/memory/archive/sessions-through-2026-03-26.md`
|
||||
|
||||
## Format
|
||||
|
||||
Each entry follows this structure:
|
||||
```
|
||||
### Session [date] — [brief description]
|
||||
**Agent(s):** [which agents were active]
|
||||
**Work completed:**
|
||||
- [bullet points of what was done]
|
||||
**Decisions made:**
|
||||
- [any design/architecture decisions with brief rationale]
|
||||
**Open questions:**
|
||||
- [anything unresolved that needs human input]
|
||||
**Next steps:**
|
||||
- [what should happen next]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Sessions
|
||||
|
||||
### Session 2026-03-31d — Phase 4 adapt + ProvidersStep/VenueStep filter panels + control bar
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Phase 4 /adapt reviews:** Ran responsive adaptation audits on Navigation, Footer, ProviderCard, VenueCard. Fixed P0/P1 touch target issues in Navigation (hamburger + drawer items minHeight 44px) and Footer (all links minHeight 44px, tagline responsive maxWidth). ProviderCard/VenueCard skipped — meta rows aren't interactive, card itself is the touch target.
|
||||
- **ProvidersStep structured filters:** Replaced generic `ProviderFilter[]` chip array with `ProviderFilterValues` interface containing 5 filter controls inside FilterPanel/DialogShell:
|
||||
- **Location:** Autocomplete `multiple` + `freeSolo` with chip-in-input pattern (real estate site UX — chip sits inside the search field with X to clear)
|
||||
- **Service tradition:** Autocomplete with type-to-search, 21 options ('None' first, then alphabetical traditions)
|
||||
- **Funeral type:** Chip multi-select with wrapping layout (6 types from spec)
|
||||
- **Verified providers / Online arrangements:** Switch toggles
|
||||
- **Price range:** Dual-knob slider + compact editable inputs (matching CoffinsStep)
|
||||
- **FilterPanel molecule updates:** "Done" → "Apply", "Clear all" moved to footer as "Reset filters" text button
|
||||
- **DialogShell padding:** px 3 → 5 (12px → 20px) for breathing room
|
||||
- **Filter panel UX iterations (3 rounds):**
|
||||
- Section headings: `labelLg` + `fontWeight: 600` for visual hierarchy
|
||||
- Price inputs: compact `fontSize: 0.875rem` + tighter padding
|
||||
- Funeral type chips: wrap layout (not horizontal scroll) — stays within bounds
|
||||
- Price slider: `px: 2.5` accommodates MUI thumb radius, no overflow
|
||||
- Active filter count includes location search; reset clears search too
|
||||
- **VenueStep structured filters:** Same FilterPanel pattern as ProvidersStep with venue-specific controls:
|
||||
- Location (chip-in-input), Venue type (7 chips: Chapel, Church, Cathedral, Outdoor Venue, Hall/Room, Mosque, Temple), Additional services (Video Streaming + Photo Display switches), Service tradition (autocomplete)
|
||||
- **Control bar (both pages):** Below search, single line:
|
||||
- `[Filters]` left, `[↕ Recommended]` right (compact menu button with dropdown)
|
||||
- Sort options: Recommended, Nearest, Price: Low to High, Price: High to Low
|
||||
- Results count on own line below with breathing room
|
||||
- **List/Map view toggle:** Floating overlay in top-left of map panel (paper bg + shadow). Text-labelled: "List" / "Map" with icons. Follows Airbnb/Domain pattern — contextually placed where the map lives.
|
||||
- New types: `ProviderSortBy`, `VenueSortBy`, `ListViewMode`, `VenueFilterValues`, `VenueTypeOption`
|
||||
|
||||
**Decisions made:**
|
||||
- "Service tradition" as the label for religion/faith filter — neutral, user-centric, covers religious and non-religious
|
||||
- 'None' as first tradition option
|
||||
- Location uses Autocomplete chip-in-input pattern (Domain/realestate.com.au style)
|
||||
- Funeral type chips wrap (stack) rather than horizontal scroll — clearer at dialog width
|
||||
- DialogShell px: 5 is a global change — all dialogs get more breathing room
|
||||
- FilterPanel footer: "Reset filters" + "Apply" replaces header "Clear all" + footer "Done"
|
||||
- List/Map toggle lives on the map panel (floating overlay), not in the control bar — frees space, contextually placed
|
||||
- Sort uses compact menu button (not TextField select) — same visual weight as Filters button
|
||||
|
||||
**Open questions:**
|
||||
- None currently
|
||||
|
||||
**Next steps:**
|
||||
- Continue user feedback on remaining pages
|
||||
- Phase 4 remaining: `/typeset` sample, `/preflight` full codebase
|
||||
- Add progress bar + cart stories to more page stories
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-31c — PaymentStep/ConfirmationStep review + progress bar + CartButton
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Organism normalize pass (Phase 3):** Scanned all 6 organisms. Fixed FuneralFinderV3 transition timing (200ms → 150ms ease-in-out), added autodocs tag to V3 stories. Navigation audit 18/20, ServiceSelector 20/20.
|
||||
- **Navigation P1 fix:** Logo wrapper changed from `div[role=link]` to proper `<a>` tag for keyboard accessibility.
|
||||
- **PaymentStep feedback (5 items):** Divider under subheading, lock icon alignment, ToggleButtonGroup `align` + `direction` props (centre-aligned payment options, vertical payment method stack), checkbox alignment fix.
|
||||
- **ToggleButtonGroup atom enhanced:** New `align` (start/center) and `direction` (row/column) props. Description text bumped from `text.secondary` to `text.primary`.
|
||||
- **SummaryStep:** Save button → text variant (matches other pages), centred.
|
||||
- **Cross-page heading spacing:** All wizard pages `mb: 1` → `mb: 2` on display3 heading.
|
||||
- **ConfirmationStep redesign:** Animated SVG tick (circle draws then checkmark), "What happens next" warm card with bullet points, contact phone as prop, link-based secondary actions.
|
||||
- **VenueStep + ProvidersStep:** Sticky search bar padding fix, off-white bg behind card lists.
|
||||
- **IntroStep, CemeteryStep, CrematoriumStep, DateTimeStep:** Dividers under subheadings.
|
||||
- **CoffinsStep:** h4 heading (matches list layouts), sidebar headings h5 → h6.
|
||||
- **CartButton molecule (new):** Outlined pill trigger with receipt icon + "Your Plan" + formatted total in brand colour. Click opens DialogShell with items grouped by section via LineItem, total row, empty state. Mobile collapses to icon + price.
|
||||
- **WizardLayout:** Removed STEPPER_VARIANTS whitelist — stepper bar now renders on all variants when props provided. Stepper centred at 700px max-width, cart hugs right.
|
||||
- **StepIndicator:** Desktop label fontSize bumped to 0.875rem for readability.
|
||||
- **Prop threading:** progressStepper + runningTotal added to DateTimeStep, VenueStep, SummaryStep, PaymentStep.
|
||||
- **Style Dictionary:** Auto-generate tokens.d.ts on build, fixed TS unused imports.
|
||||
|
||||
**Decisions made:**
|
||||
- ToggleButtonGroup `align` and `direction` are opt-in props — existing usages unchanged
|
||||
- ConfirmationStep contact phone is now a prop (default 1800 987 888), no more hardcoded placeholder
|
||||
- WizardLayout stepper bar shows on ANY variant when props provided (StepperBar returns null when empty)
|
||||
- CartButton is a molecule (not organism) — self-contained trigger + dialog, same pattern as FilterPanel
|
||||
- Stepper bar layout: stepper centred at maxWidth 700px, cart hugs right edge
|
||||
|
||||
**Open questions:**
|
||||
- PaymentStep CTA is right-aligned — all other pages use full-width. Should it match?
|
||||
- Progress bar stories should be added to more page stories (currently only DateTimeStep has them)
|
||||
|
||||
**Next steps:**
|
||||
- Continue user feedback/edits on remaining pages
|
||||
- Add progress bar + cart to remaining page stories
|
||||
- Retroactive review Phase 3 organisms: Navigation P2s still pending (hardcoded fontSize, drawer width)
|
||||
- Update component-registry.md with CartButton
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-31b — CoffinDetailsStep rewrite + AdditionalServicesStep split
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **CoffinDetailsStep rewrite** — two-panel detail-toggles layout with gallery + specs (left), name + description + colour swatches + allowance-aware pricing + CTA (right). Colour selection doesn't affect price. Allowance logic: fully covered / partially covered / no allowance.
|
||||
- **SummaryStep rewrite** — visual cart layout replacing accordion text lists:
|
||||
- Arrangement details section at top (arranger, deceased, tradition, dates, times)
|
||||
- Compact image cards for provider, venue, crematorium, coffin with location pins
|
||||
- Allowance display: "Included in your package" (fully covered) vs price/allowance/remaining breakdown
|
||||
- Checklist for included services, priced list for extras (consistent tick logic)
|
||||
- Share dialog: "Share this plan" opens DialogShell with multi-email input + confirmation
|
||||
- Full-width CTA, deposit deferred to PaymentStep
|
||||
- **AdditionalServicesStep split into two pages:**
|
||||
- **IncludedServicesStep (new)** — services included in the package at no cost. Dressing, viewing (with same-venue sub-option inside card), prayers/vigil, funeral announcement.
|
||||
- **ExtrasStep (new)** — optional paid extras for lead generation. Catering, music (flat inline live musician toggle + musician type), coffin bearing (toggle + bearer preference radio), newspaper notice. POA support via `priceLabel`. Tally of priced selections.
|
||||
- **AddOnOption molecule enhanced:**
|
||||
- `children` prop — sub-options render inside the card boundary (below divider) when checked, eliminating nested card "Russian doll" pattern
|
||||
- `priceLabel` prop — custom text like "Price on application" in brand copper italic
|
||||
- **AdditionalServicesStep removed** — replaced by the two new pages
|
||||
- All quality checks passing (TypeScript, ESLint, Prettier)
|
||||
- Playwright visual verification of all key scenarios
|
||||
|
||||
**Decisions made:**
|
||||
- Split AdditionalServicesStep into two pages for clearer UX distinction between free inclusions and paid extras
|
||||
- Sub-options render inside parent card (flat hierarchy) instead of nested cards
|
||||
- Coffin bearing changed from always-visible radio to toggle + sub-options (consistent with other items)
|
||||
- `bearing` field split into `bearing: boolean` + `bearerType` for toggle pattern
|
||||
- Extras page is lead-gen: signals interest, not firm commitment. Director follows up.
|
||||
- POA items show "Price on application" in brand copper italic
|
||||
- Copy refined through brand lens — no transactional language ("toggle on"), warm professional tone
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Continue page feedback: PaymentStep, ConfirmationStep
|
||||
- Retroactive review Phase 3 (organisms) still pending
|
||||
- Batch a11y fix (aria-describedby + aria-invalid) deferred
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-31b — CoffinDetailsStep rewrite: product detail layout
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **CoffinDetailsStep complete rewrite** — transformed to match VenueDetailStep two-panel pattern:
|
||||
- **Left panel:** ImageGallery (hero + thumbnails), product details as semantic `dl` list (bold label above value)
|
||||
- **Right panel (sticky):** coffin name (h1), description, colour swatch picker, price with allowance-aware display, CTA, save-and-exit
|
||||
- **Colour picker:** circular swatches (36px), `aria-pressed`, controlled via `selectedColourId`/`onColourChange`, does not affect price
|
||||
- **Allowance pricing logic:** fully covered (allowance >= price) → "Included in your package allowance — no change to your plan total." / partially covered → shows "$X package allowance applied" + "+$Y to your plan total" in brand colour / no allowance → price only with extra spacing to CTA
|
||||
- **Removed:** info bubble (redundant with allowance impact text), `priceNote` prop, `termsText` prop, old horizontal specs grid, `CoffinAllowance` type
|
||||
- **Added:** `CoffinColour` type, `allowanceAmount` prop, `onAddCoffin` callback (replaces `onContinue`)
|
||||
- **A11y:** fixed heading hierarchy (price as `<p>` not `<h5>`, Product details as `<h2>`) — 0 violations
|
||||
- **Stories:** FullyCovered, PartiallyCovered, NoAllowance, NoColours, Minimal, PrePlanning, Loading
|
||||
- **Playwright visual verification** of all key scenarios
|
||||
- All quality checks passing (TypeScript, ESLint, Prettier)
|
||||
|
||||
**Decisions made:**
|
||||
- Allowance impact is computed from `allowanceAmount` vs `coffin.price` — no remaining balance tracking (out of scope)
|
||||
- Info bubble removed from detail page (redundant) — kept on CoffinsStep browsing page
|
||||
- Product details as single-column stacked `dl` (label above value) — more readable than grid
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Continue page feedback: AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
- Retroactive review Phase 3 (organisms) still pending
|
||||
- Batch a11y fix (aria-describedby + aria-invalid) deferred
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-31a — CoffinsStep rewrite: grid-sidebar ecommerce layout
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **CoffinsStep complete rewrite** — transformed from wide-form card grid into a two-column ecommerce layout matching the live site pattern:
|
||||
- **Layout:** `grid-sidebar` with viewport-locked independent scrolling (sidebar scrollbar on hover, grid always scrollable)
|
||||
- **Sidebar:** heading, conditional allowance info bubble, category menu with expandable subcategories (Collapse animation, chevron rotation), dual-knob price range slider with editable text inputs (commit on blur/Enter), sort by dropdown (popularity, price asc/desc), clear all filters link, save-and-exit link at bottom
|
||||
- **Grid:** 3-col coffin card grid with `CoffinCard` internal component — thumbnail hover preview (hover swaps main image, leave reverts), equal-height cards via flex layout, subtle background for white-bg product photos, "Most Popular" badge, colour count text
|
||||
- **Navigation:** card click fires `onSelectCoffin(id)` to navigate to CoffinDetailsStep — no Continue button
|
||||
- **Pagination:** 20 coffins per page max (`pageSize` prop, enforced via slice)
|
||||
- **WizardLayout `grid-sidebar` update** — wider sidebar (28%), `overflowX: hidden` (no horizontal scroll), `overflowY: auto` at all breakpoints, responsive viewport-lock (desktop only)
|
||||
- **Removed:** `selectedCoffinId`, `CoffinsStepErrors`, `CoffinPriceRange`, `onContinue`, `loading` props
|
||||
- **Added:** `onSelectCoffin`, `CoffinSortBy`, `pageSize`, `minPrice`/`maxPrice`, `thumbnails`/`colourCount` on Coffin, `children` on `CoffinCategory`
|
||||
|
||||
**Decisions made:**
|
||||
- Card click navigates directly to details (no selection + continue pattern) — matches ecommerce browsing UX
|
||||
- Categories support one level of subcategories — parent click loads all and expands children, child click refines
|
||||
- Price slider with editable text inputs — commits on blur/Enter to avoid per-keystroke re-renders
|
||||
- 20 coffins per page default — enforced in component via slice
|
||||
- Allowance info bubble is conditional (omit prop = no bubble) — some packages don't have coffin allowances
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Continue page feedback: AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
- Retroactive review Phase 3 (organisms) still pending
|
||||
- Batch a11y fix (aria-describedby + aria-invalid) deferred
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-30e — Page feedback: VenueServices, Crematorium, Cemetery rewrites
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **VenueServicesStep (new page):** Built step 7c — venue-specific service toggles after VenueDetailStep. Compact venue card at top, availability notices (info cards for unavailable services), AddOnOption toggles with maxDescriptionLines for "Read more" expansion, conditional tally showing total of selected priced services. Flow: VenueStep → VenueDetailStep → VenueServicesStep.
|
||||
- **AddOnOption price colour fix:** Changed from `text.secondary` (grey) to `color="primary"` (copper) for consistency with all other price displays in the system.
|
||||
- **CrematoriumStep rewrite:** Completely rewritten to match live site. Two variants based on funeral type:
|
||||
- Service & Cremation: stacked crematorium card (image top, details below) + witness Yes/No/Decide later ToggleButtonGroup
|
||||
- Cremation Only: stacked card + "Cremation Only" badge + "Included in Package" info notice, no witness question
|
||||
- Removed: multi-card selection grid, priority dropdown, special instructions, crematoriums array prop
|
||||
- Card iterated from compact horizontal → more generous horizontal → stacked layout based on user feedback
|
||||
- **CemeteryStep rewrite:** Rewritten to match live site:
|
||||
- RadioGroups replaced with ToggleButtonGroups (Yes/No/Not sure)
|
||||
- Card selection grid replaced with freetext search input (Input atom with search icon)
|
||||
- `searchSlot` prop for parent to inject Google Places or geocoding autocomplete
|
||||
- Removed static `cemeteries` array prop and `CemeteryOption` type
|
||||
- Progressive disclosure preserved: own plot → locate it; no plot → preference? → search
|
||||
|
||||
**Decisions made:**
|
||||
- Venue services belong on a separate page (not inline on VenueDetailStep or in a dialog) due to content volume per service (long descriptions, future images)
|
||||
- Crematorium is always pre-selected by provider — no multi-select needed, just confirmation
|
||||
- "Decide later" / "Not sure" options added as explicit third choice on questions where users may be uncertain — grief-sensitive pattern
|
||||
- Cemetery search should use real location search (Google Places style), not a static dropdown — we don't maintain a cemetery database
|
||||
- Stacked card layout (image top) works better than compact horizontal for single confirmation cards with long text
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Continue page feedback: CoffinsStep, AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
- Retroactive review Phase 3 (organisms) still pending
|
||||
- Batch a11y fix (aria-describedby + aria-invalid) deferred
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-30d — Steps 8-15 consistency pass
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Consistency pass across Steps 8-15** — aligned all remaining wizard steps with the conventions established during the Steps 1-6 feedback iteration:
|
||||
- **Subheading variant:** CrematoriumStep, CoffinDetailsStep changed from `body2` → `body1` (match IntroStep/DateTimeStep centered-form convention)
|
||||
- **Subheading spacing:** CrematoriumStep, CemeteryStep, CoffinDetailsStep, AdditionalServicesStep, SummaryStep, PaymentStep all changed from `mb: 4` → `mb: 5`
|
||||
- **Section dividers:** CrematoriumStep gained 3 `<Divider>` between question sections (crematorium → witness, conditional priority, special instructions)
|
||||
- **Link atom:** PaymentStep terms links and ConfirmationStep phone number changed from `Box component="a"` → `Link` atom
|
||||
- **CoffinsStep:** No changes needed (wide-form layout has its own spacing convention)
|
||||
- **Audited CrematoriumStep (15/20) and PaymentStep (16/20)** — findings are pre-existing, not introduced by the pass
|
||||
- **Visual verification:** Playwright screenshots of CrematoriumStep, PaymentStep, ConfirmationStep, AdditionalServicesStep, SummaryStep — all pass
|
||||
|
||||
**Decisions made:**
|
||||
- Centered-form heading convention now formally: `display3` → `body1` subheading → `mb: 5` gap (documented via consistent implementation across all 10+ centered-form steps)
|
||||
- Wide-form layout (CoffinsStep) keeps its own tighter spacing convention (`body2` + `caption`, `mb: 3`)
|
||||
|
||||
**Open questions:**
|
||||
- **Checkbox atom:** PaymentStep uses MUI Checkbox directly — only form control without an FA atom wrapper. Create Checkbox atom?
|
||||
- **aria-describedby/aria-invalid:** All wizard steps lack error field association. Batch normalize fix recommended.
|
||||
- **Retroactive review Phase 2.2** still pending (audit priority molecules: ServiceOption, AddOnOption, ProviderCardCompact)
|
||||
|
||||
**Also completed (same session):**
|
||||
- **Checkbox atom created:** New FA wrapper (forwardRef, displayName, stories: Default/States/TermsAgreement/Checklist). MuiCheckbox theme overrides (warm gold checked, focus ring, disabled muted). PaymentStep updated to import from atom.
|
||||
- **Retroactive review Phase 2.2 complete:** Audited ServiceOption (13/20), AddOnOption (14/20), ProviderCardCompact (15/20). Fixed:
|
||||
- AddOnOption: added `aria-disabled` when disabled (P1)
|
||||
- ProviderCardCompact: added `maxLines={1}` on name (P2)
|
||||
- ServiceOption: price uses `labelLg` variant instead of h6 + hardcoded fontWeight (P1)
|
||||
- Note: audit agents flagged false P0s (AddOnOption "double handler", ServiceOption "role collision") — verified both are correctly handled by existing `stopPropagation` and prop spread order in Card atom
|
||||
|
||||
**Also completed (continued same session):**
|
||||
- **DateTimeStep:** Swapped date TextField to Input atom (was missed in session 30b)
|
||||
- **VenueStep rewrite:** Simplified to click-to-navigate (like ProvidersStep) — removed Continue, selection state, inline detail, service toggles
|
||||
- **VenueDetailStep (new page):** Detail-toggles layout with scrollable left panel (ImageGallery, description, features grid, location map placeholder, address, informational service cards) and right panel (name, meta icons, price + offset note, Add Venue CTA, address, religion chips)
|
||||
- **CoffinDetailsStep:** Switched from centered-form to detail-toggles layout (image left, specs/price/CTA right)
|
||||
- **WizardLayout detail-toggles overhaul:** Viewport-locked with independent panel scroll, maxWidth 1200px centered, generous padding (px:5), back link inside left panel
|
||||
- **WizardLayout list-map scroll fix:** Removed top padding gap between nav and sticky search, heading scrolls away while search/filters stay pinned with divider border
|
||||
- **ImageGallery molecule (new):** Hero image + landscape thumbnail strip (4:3 ratio), hover to preview, click to select, keyboard accessible, wired into VenueDetailStep and CoffinDetailsStep
|
||||
- **Checkbox atom (new):** FA wrapper with warm gold theming, PaymentStep updated
|
||||
- **Retroactive review Phase 2.2:** Audited ServiceOption (13/20), AddOnOption (14/20), ProviderCardCompact (15/20), fixed P0/P1s
|
||||
|
||||
**Next steps:**
|
||||
- Continue page feedback from user: CrematoriumStep, CemeteryStep, CoffinsStep, AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
- Venue service selection step (new page — toggles moved from VenueDetailStep)
|
||||
- Batch a11y fix: aria-describedby + aria-invalid across all wizard steps (deferred per user)
|
||||
- Retroactive review Phase 3: organisms normalize + audit
|
||||
- Update component registry with new components (ImageGallery, Checkbox, VenueDetailStep)
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-30c — HomePage build + Chromatic + Story to Design setup
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Story to Design agent:** Helped user install and run the story.to.design agent on Arch Linux (extracted from .deb, installed webkit2gtk dependency). Agent connects Storybook to Figma plugin.
|
||||
- **Chromatic setup:** Installed chromatic, published Storybook to Chromatic CDN. Set visibility to public for shareable links. Updated `package.json` chromatic script with `--build-script-name=build:storybook`.
|
||||
- **HomePage component built:** Full marketing landing page at `src/components/pages/HomePage/`
|
||||
- 7 sections: Hero, FuneralFinder widget (overlapping card), Partner Logos carousel, Features (4-card grid), Reviews/Testimonials, CTA Banner, FAQ accordion
|
||||
- Composes FuneralFinderV3 organism, Navigation + Footer via props
|
||||
- CSS-only infinite scroll logo carousel with prefers-reduced-motion
|
||||
- Feature cards with warm circular icon backgrounds
|
||||
- Full a11y: section landmarks, aria-labelledby, heading hierarchy
|
||||
- Grief-sensitive copy (quieter pass applied)
|
||||
- **Quality gates run:** /critique (32/40), /audit (16/20), /quieter (all pass)
|
||||
- **Design iteration:** Multiple rounds of visual refinement using Playwright screenshots
|
||||
- Reduced oversized typography (display1 → display3 for hero, display2 → h2 for sections)
|
||||
- Reserved serif fonts for hero + CTA only, sans-serif for all other section headings
|
||||
- Tightened spacing, reduced section padding
|
||||
- Feature cards: compact padding, warm circular icon backgrounds
|
||||
- Fixed unicode rendering bugs in copy
|
||||
|
||||
**Decisions made:**
|
||||
- Serif display fonts reserved for hero heading and CTA banner only — all other section headings use Montserrat (h2/h3)
|
||||
- Feature card icons wrapped in warm circular backgrounds (brand.50 bg, 56px diameter)
|
||||
- HomePage is a page component (not a template) — does not use WizardLayout
|
||||
- Chromatic published URL is the shareable Storybook link for stakeholders
|
||||
|
||||
**Open questions:**
|
||||
- Homepage is first-pass only — user will return to iterate later
|
||||
- Partner logo placeholder images need real assets
|
||||
- Hero image slot needs real photography
|
||||
- CTA banner click destination needs clarification (scroll to widget vs wizard entry)
|
||||
|
||||
**Next steps:**
|
||||
- Resume arrangement wizard page feedback from VenueStep (Step 7) onwards — Steps 1-6 done in previous session
|
||||
- Remaining pages: VenueStep, CrematoriumStep, CemeteryStep, CoffinsStep, CoffinDetailsStep, AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-30b — Page feedback iteration (Steps 1-6) + DialogShell
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **IntroStep (Step 1):** Static subheading (removed dynamic text change on selection), ToggleButtonGroup top-left alignment fix
|
||||
- **ProvidersStep (Step 2):** Heading reduced to h4 "Find a funeral director", SearchBar → location TextField with pin icon, FilterPanel moved below search right-aligned, map fill fix (height:100% → flex:1), hover scrollbar on left panel
|
||||
- **VenueStep (Step 7):** Same consistency fixes as ProvidersStep (h4, location icon, filter layout, map fill, results count format)
|
||||
- **PackagesStep (Step 3):** Removed budget filter + "Most Popular" badge + mobile Continue button. Added grouped packages pattern ("Matching your preferences" / "Other packages from [Provider]" with brand accent bars). `onArrange` replaces `onContinue`. Clickable provider card (`onProviderClick`). Heading to h4.
|
||||
- **DateTimeStep (Step 6):** display3 heading (centered-form consistency). Name fields swapped from MUI TextField to Input atom (external label, no clipping). Multiple preferred dates (up to 3, progressive disclosure). Removed service tradition/religion field. Dividers between question sections.
|
||||
- **DialogShell atom:** New shared dialog container (header + optional back + close, scrollable body, optional footer). Audited 17→19/20, P1s fixed (focus management, sx pattern).
|
||||
- **FilterPanel refactored:** Popover → DialogShell (centered Dialog with backdrop)
|
||||
- **ArrangementDialog refactored:** Now composes DialogShell for consistent chrome
|
||||
- **PreviewStep + AuthGateStep removed:** Consolidated into ArrangementDialog (D-E)
|
||||
- **WizardLayout:** ListMapLayout left panel gets thin scrollbar visible on hover
|
||||
|
||||
**Decisions made:**
|
||||
- Heading convention: display3 for centered-form pages, h4 for narrow panels (list-map, list-detail)
|
||||
- DialogShell is the standard popup container — all site dialogs compose it
|
||||
- Service tradition removed from DateTimeStep — flows from provider/package selection, confirmed on summary
|
||||
- "Service tradition" is the preferred terminology (not "religion" or "religious style")
|
||||
- Package grouping pattern: matched vs other, with brand accent bar section labels
|
||||
- FilterPanel uses Dialog (not Popover) for filter controls
|
||||
|
||||
**Open questions:**
|
||||
- Provider profile popup contents — user will provide details later
|
||||
- Heading convention (display3 vs h4) should be documented as a decision
|
||||
|
||||
**Next steps:**
|
||||
- **Resume page feedback from VenueStep (Step 7) onwards** — Steps 1-6 are done
|
||||
- Remaining pages to review: VenueStep, CrematoriumStep, CemeteryStep, CoffinsStep, CoffinDetailsStep, AdditionalServicesStep, SummaryStep, PaymentStep, ConfirmationStep
|
||||
- Homepage layout work (user priority — separate task)
|
||||
- Input atom clipping: audit remaining pages for MUI TextField → Input atom swap where labels clip
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-30a — Tooling upgrades + workflow evaluation
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Storybook addons:** Added `@storybook/addon-storysource` (story source code panel) and `@storybook/addon-a11y` (real-time axe-core accessibility checks)
|
||||
- **Playwright MCP:** Installed `@playwright/mcp` and registered as MCP server. Enables Claude Code to take screenshots of Storybook for visual verification. Available from next session.
|
||||
- **Workflow evaluation:** Comprehensive analysis of current capabilities, identified core gap (Claude Code cannot see visual output), researched solutions
|
||||
- **Antigravity research:** Investigated Google Antigravity IDE for visual polish workflow. Installed on system. Created `GEMINI.md` project rules file. User decided to pause on Antigravity for now and continue with Claude Code + Playwright MCP approach.
|
||||
- **GEMINI.md:** Created project rules file for Antigravity IDE, mirroring key conventions from CLAUDE.md (token access, atomic tiers, no hardcoded values)
|
||||
|
||||
**Decisions made:**
|
||||
- Playwright MCP is the primary solution for visual verification gap — enables Claude Code to screenshot Storybook stories and self-correct visual issues
|
||||
- Antigravity available as secondary tool for visual tweaks but not primary workflow
|
||||
- Claude Code remains the primary tool for architectural/structural work due to custom skills, memory system, and accumulated project context
|
||||
|
||||
**Open questions:**
|
||||
- User has refinement feedback from previous session that was partially overlooked — to be addressed next session
|
||||
- Some items from the user's original feedback across 15 steps were missed when compressed into D-A through D-H decisions
|
||||
|
||||
**Next steps:**
|
||||
- Restart session to activate Playwright MCP
|
||||
- User to provide refinement feedback
|
||||
- Use Playwright MCP to visually verify changes as they're made
|
||||
- Address remaining P1s from previous session (radiogroup keyboard nav, dialog mobile fullscreen)
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-29e — Feedback iteration Batches 1 & 2
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Batch 1: Atom + Template Foundation**
|
||||
- ToggleButtonGroup: label-to-options spacing `mb: 1→2`, top-align content `flex-start`, fixed selected border CSS specificity (added `&.Mui-selected` in grouped selector)
|
||||
- Heading standardisation: all 6 split-layout steps changed from `h4` → `display3` (ProvidersStep, PackagesStep, PreviewStep, VenueStep, CoffinsStep, CoffinDetailsStep) per D-A
|
||||
- DateTimeStep: normalised section gaps (scheduling fieldset `mb: 3→4`)
|
||||
- CrematoriumStep: added subheading for consistency, normalised witness section `mb: 3→4`
|
||||
- PackagesStep + DateTimeStep: fixed input label clipping (`pt: 0.5` on TextField containers)
|
||||
- **Batch 2: List-Map Layout Rework (D-B)**
|
||||
- WizardLayout ListMapLayout: 420px fixed left column, `flex: 1` right panel
|
||||
- Back link rendered inside left panel (not above split) — eliminates gap above map
|
||||
- LAYOUT_MAP type updated to accept `backLink` prop for list-map variant
|
||||
- ProvidersStep + VenueStep: sticky header (heading + search + filters pinned at top of scrollable left panel)
|
||||
- **Batch 3: FilterPanel molecule + integration (D-C, D-F)**
|
||||
- New FilterPanel molecule: Popover trigger with active count badge, Clear all, Done actions
|
||||
- ProvidersStep: inline chips → FilterPanel Popover alongside search bar
|
||||
- VenueStep: same pattern, filter chips in Popover
|
||||
- CoffinsStep (D-F): grid-sidebar → wide-form (full-width 4-col grid), filters in Popover
|
||||
- WizardLayout: added `wide-form` variant (maxWidth lg, single column) for card grids
|
||||
- FilterPanel stories: Default, WithActiveFilters, SelectFilters, CustomLabel
|
||||
- **Batch 4: Step Simplifications (D-D, D-G)**
|
||||
- ProvidersStep (D-D): removed selection state, Continue button, radiogroup pattern. Clicking a provider card triggers navigation directly. Stories updated.
|
||||
- CoffinDetailsStep (D-G): removed all customisation (handles, lining, nameplate, OptionSection, ProductOption/CoffinDetailsStepValues types). Simplified to coffin profile + Continue CTA. Changed from detail-toggles to centered-form layout. Stories simplified.
|
||||
- Updated CoffinDetailsStep index.ts re-exports
|
||||
- **Batch 5: ArrangementDialog organism (D-E)**
|
||||
- New ArrangementDialog organism: MUI Dialog with two internal steps
|
||||
- Step 1 (preview): ProviderCardCompact, package summary with sections/items/total, "What happens next" checklist
|
||||
- Step 2 (auth): SSO buttons, email, progressive disclosure for details, verification code, terms
|
||||
- Parent controls step state + auth form values; dialog has back arrow and close button
|
||||
- Stories: Default (full flow), AuthStep, AuthDetails, PrePlanning
|
||||
- PreviewStep + AuthGateStep kept for now, to be deprecated once dialog is wired in
|
||||
|
||||
**Decisions made:**
|
||||
- All 8 iteration decisions now implemented: D-A through D-H
|
||||
|
||||
**Quality passes:**
|
||||
- Ran 3 parallel audits (FilterPanel, ArrangementDialog, all reworked steps)
|
||||
- FilterPanel: 4 P0 + 2 P1 fixed (forwardRef, useId, aria-controls, dialog role, label prop, scroll)
|
||||
- ArrangementDialog: 3 P0 + 2 P1 fixed (forwardRef, focus management, aria-live, semantic tokens, Link atom)
|
||||
- Sticky headers: responsive padding match + top breathing room (pt: 2)
|
||||
- ProvidersStep: role="list", semantic map token
|
||||
- VenueStep: aria-label on search, semantic map token
|
||||
- CoffinDetailsStep: form wrapper added
|
||||
|
||||
**Remaining P1/P2 (for next session):**
|
||||
- VenueStep/CoffinsStep radiogroup arrow-key navigation (P1)
|
||||
- ArrangementDialog step number badge hardcoded font size/weight (P1)
|
||||
- ArrangementDialog mobile fullscreen treatment (P1)
|
||||
- CoffinDetailsStep centered-form width may be tight for coffin images (P2)
|
||||
- CTA alignment inconsistency VenueStep vs CoffinsStep/CoffinDetailsStep (P2)
|
||||
- ArrangementDialog missing story variants: verify, loading, errors, overflow (P2)
|
||||
|
||||
**Open questions:**
|
||||
- PreviewStep + AuthGateStep: deprecate/remove once ArrangementDialog fully replaces them in the flow
|
||||
- User to review all steps in Storybook and report any visual issues still present
|
||||
|
||||
**Next steps:**
|
||||
- User review in Storybook — note any remaining visual issues
|
||||
- Fix remaining P1s (radiogroup keyboard nav, dialog mobile fullscreen)
|
||||
- Wire ArrangementDialog into PackagesStep flow
|
||||
- Component registry updates for FilterPanel + ArrangementDialog
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-29c — Grooming pass: critique/harden/polish all 15 steps
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Full grooming pass across all 15 wizard steps** — applied critique (UX design review), harden (edge cases/error states), and polish (visual alignment/copy/code) frameworks.
|
||||
- **[P0] CrematoriumStep bug fix:** `<option>` elements replaced with `<MenuItem>` — the MUI `<TextField select>` requires MenuItem children, not native option elements. The priority dropdown was non-functional.
|
||||
- **[P1] Double-submit prevention:** Added `if (!loading) onContinue()` guard to all form `onSubmit` handlers across steps 1, 5, 6, 7, 8, 9, 10, 11, 12, 14 (10 steps with forms). Prevents keyboard Enter re-submission during async loading.
|
||||
- **[P1] Accessibility — aria-busy:** Added `aria-busy={loading}` to all form elements so screen readers announce processing state.
|
||||
- **[P1] Error colour normalisation:** Replaced `color="error"` (red #BC2F2F) with `var(--fa-color-text-brand)` (copper #B0610F) across 7 steps (ProvidersStep, PackagesStep, VenueStep, CrematoriumStep, CemeteryStep, CoffinsStep, PaymentStep). Per D024, FA uses warm copper for errors rather than aggressive red.
|
||||
- **[P2] Grief-sensitive copy:** "Has the person died?" → "Has this person passed away?" (IntroStep). "About the person who died" → "About the person who has passed" (DateTimeStep).
|
||||
- **[P2] Results count copy:** "Showing results from X providers" → "X providers found" (ProvidersStep).
|
||||
- **[P2] Empty state guidance:** Added actionable guidance text to empty states in ProvidersStep, PackagesStep, VenueStep, CoffinsStep ("Try adjusting..." copy).
|
||||
- **Steps that passed with no issues:** PreviewStep (4), SummaryStep (13), ConfirmationStep (15) — all clean.
|
||||
- **IntroStep critique score: 35/40 (Good)** — full heuristic scoring completed as reference.
|
||||
- **D034 implemented:** Form error styling unified to copper end-to-end. MuiOutlinedInput error border/ring, MuiFormHelperText error text, MuiFormLabel error state (neutral per D024), ToggleButtonGroup error border — all now use copper (#B0610F). `palette.error.main` remains red for non-form uses.
|
||||
- **Tagged `v0.1-wizard-groomed`** — safe rollback point before user review checkpoint.
|
||||
- **Roadmap documented** in persistent memory (`project_roadmap.md`) with 9 phases, checkpoint gates, and guiding principles.
|
||||
|
||||
**Decisions made:**
|
||||
- D034: Form error styling uses copper across theme (MuiOutlinedInput, MuiFormHelperText, MuiFormLabel, ToggleButtonGroup). `palette.error.main` stays red for destructive buttons/system alerts.
|
||||
- User review checkpoint required before integration — tagged `v0.1-wizard-groomed` as rollback point.
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Feedback iteration Batch 1 (next session)
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-29d — User review checkpoint + iteration planning
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **D034 implemented:** Form error copper styling unified across theme (MuiOutlinedInput, MuiFormHelperText, MuiFormLabel, ToggleButtonGroup).
|
||||
- **Tagged `v0.1-wizard-groomed`** — safe rollback point before feedback iteration.
|
||||
- **Roadmap documented** in persistent memory (`project_roadmap.md`).
|
||||
- **User review checkpoint completed.** User reviewed all 15 steps in Storybook and provided detailed feedback across every step.
|
||||
- **Iteration plan created** at `.claude/plans/zany-jingling-lynx.md` — 7 batches, ~6-8 sessions estimated.
|
||||
- **All 8 design decisions resolved** (D-A through D-H): display3 headings everywhere, 420px fixed left column, Popover filters, click-to-navigate (no Continue buttons on selection steps), two-step modal for preview+auth, full-width coffin grid, remove coffin customisation for now, defer map pins.
|
||||
|
||||
**Key feedback themes:**
|
||||
- ToggleButtonGroup: spacing tight, content misaligned, selected border not showing (CSS specificity bug)
|
||||
- List-map layout: needs fixed-width left column, scrollable list with fixed header, no gap above map
|
||||
- New FilterPanel component needed (reusable across providers, venues, coffins)
|
||||
- PreviewStep + AuthGateStep consolidation into two-step modal
|
||||
- CoffinDetailsStep: remove customisation, simplify to details + CTA
|
||||
- Several copy and spacing issues across steps
|
||||
|
||||
**Decisions made:**
|
||||
- D-A: `display3` heading everywhere (brand warmth)
|
||||
- D-B: 420px fixed left column in list-map layout
|
||||
- D-C: FilterPanel uses Popover (MVP), Drawer for mobile later
|
||||
- D-D: Click-to-navigate on ProvidersStep (remove selection state + Continue)
|
||||
- D-E: Remove standalone PreviewStep + AuthGateStep, use two-step ArrangementDialog
|
||||
- D-F: CoffinsStep full-width grid with FilterPanel button
|
||||
- D-G: Remove coffin customisation, note as future enhancement
|
||||
- D-H: Map pins deferred
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Start Batch 1: Atom + Template Foundation (ToggleButtonGroup fixes, heading standardisation, spacing normalisation)
|
||||
- Plan file: `.claude/plans/zany-jingling-lynx.md`
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-29b — Wizard steps 5-15 (complete flow)
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Step 5 — AuthGateStep:** Centered-form, 3 progressive sub-steps (SSO/email → details → verify). Phone optional when email-only. Benefit framing ("Save your plan"). Audit 18/20, P1 fixed (responsive name fields).
|
||||
- **Step 6 — DateTimeStep:** Centered-form, two fieldset sections (name + scheduling). Grief-sensitive labels ("Their first name"). Date/time radios + Autocomplete for religion (22 options). Save-and-exit CTA.
|
||||
- **Step 7 — VenueStep:** Venue card grid (1-2 col) with search/filters. Inline venue detail (Collapse) with features + religions. Service toggles (photo, streaming, recording) via AddOnOption. VenueCard extended with selected + ARIA passthrough props.
|
||||
- **Step 8 — CrematoriumStep:** Single crematorium confirmation card (most common) or multi-card grid. Witness question personalised with deceased name. Special instructions via progressive disclosure textarea.
|
||||
- **Step 9 — CemeteryStep:** Triple progressive disclosure (have plot? → choose cemetery? → card grid). Dependent field resets. "Provider can arrange this" shortcut.
|
||||
- **Step 10 — CoffinsStep:** 3-col card grid with category/price filters. "Most Popular" badge (Rec #10). Pagination support. Australian terminology ("coffin").
|
||||
- **Step 11 — CoffinDetailsStep:** Coffin profile (image + specs grid) + 3 product option RadioGroups (handles, lining, nameplate) with branded selected state. Price: "Included" or "+$X".
|
||||
- **Step 12 — AdditionalServicesStep:** Merged from optionals + extras (Rec #2). Section 1: complimentary (dressing, viewing, prayers, announcement). Section 2: paid (catering, music, bearing, newspaper). Multi-level progressive disclosure.
|
||||
- **Step 13 — SummaryStep:** Accordion sections with edit buttons, dl/dt/dd definition lists, total bar (aria-live), share button. Pre-planning: "Save your plan" / at-need: "Confirm".
|
||||
- **Step 14 — PaymentStep:** ToggleButtonGroup for plan (full/deposit) + method (card/bank). PayWay iframe slot. Bank transfer details display. Terms checkbox. Security reassurance.
|
||||
- **Step 15 — ConfirmationStep:** Terminal page, no back/progress. At-need: "submitted" + callback info. Pre-planning: "saved" + return-anytime. Muted success icon, next-steps links.
|
||||
|
||||
**Approach:** Same as previous session — first pass build + quick audit + P0/P1 fixes only. Grooming pass comes later.
|
||||
|
||||
- **Layout fixes:** Corrected 3 steps that had wrong WizardLayout variants:
|
||||
- VenueStep: centered-form → **list-map** (venue cards left, map slot right — matches ProvidersStep)
|
||||
- CoffinsStep: centered-form → **grid-sidebar** (filter sidebar left, card grid right)
|
||||
- CoffinDetailsStep: centered-form → **detail-toggles** (coffin profile left, option selectors right)
|
||||
|
||||
**Decisions made:**
|
||||
- VenueCard extended with selected/ARIA props (same pattern as ProviderCard from step 2)
|
||||
- CoffinDetailsStep uses OptionSection helper component for DRY RadioGroup rendering
|
||||
- SummaryStep uses Accordion not Paper for sections (per spec's collapsible requirement)
|
||||
- PaymentStep uses slot pattern for PayWay iframe (cardFormSlot prop)
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- All 15 wizard steps are now first-pass complete (steps 1-15) with correct layouts
|
||||
- Grooming pass: /critique, /harden, /polish, /normalize across all steps
|
||||
- Retroactive review backlog still pending
|
||||
- Integration: routing, state management, GraphQL queries
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-29 — Wizard Phase 0 through Phase 4 (foundation + steps 1-4)
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Phase 0 — Foundation components:**
|
||||
- WizardLayout template: 5 layout variants, nav slot, sticky help bar, back link, progress stepper + running total, `<main>` landmark
|
||||
- Collapse atom: thin MUI Collapse wrapper with unmountOnExit
|
||||
- ToggleButtonGroup atom: exclusive single-select with fieldset/legend a11y, FA brand styling
|
||||
- **Phase 1 — IntroStep (step 1):**
|
||||
- Centered-form layout, forWhom + hasPassedAway with progressive disclosure
|
||||
- Auto-sets hasPassedAway="no" for "Myself", grief-sensitive copy adapts
|
||||
- Audit 18→20/20 after fixes (`<main>` landmark, `<form>` wrapper)
|
||||
- **Phase 2 — ProvidersStep (step 2):**
|
||||
- List-map split: provider cards w/ radiogroup + search + filter chips (left), map slot (right)
|
||||
- ProviderCard extended with selected prop + HTML/ARIA passthrough
|
||||
- Audit 18/20, P1 fixed (radio on focusable Card)
|
||||
- **Phase 3 — PackagesStep (step 3):**
|
||||
- List-detail split: ProviderCardCompact + budget filter + ServiceOption list (left), PackageDetail (right)
|
||||
- "Most Popular" badge, mobile Continue button
|
||||
- **Phase 4 — PreviewStep (step 4):**
|
||||
- Informational review, "What happens next" numbered checklist
|
||||
- Pre-planning variant shows "Explore other options" tertiary CTA
|
||||
|
||||
**Approach:** First pass of all steps (build + quick audit + P0/P1 fixes only), then grooming pass (critique/harden/polish/normalize) across the full flow.
|
||||
|
||||
**Decisions made:**
|
||||
- Templates at `src/components/templates/`, Pages at `src/components/pages/`
|
||||
- ProviderCard/ServiceOption: ARIA passthrough for radiogroup patterns
|
||||
- First-pass approach: build all steps, then groom — user confirmed
|
||||
|
||||
**Open questions:**
|
||||
- None
|
||||
|
||||
**Next steps:**
|
||||
- Step 5 (Auth Gate) — centered-form layout, account creation/login
|
||||
- Continue through remaining steps (6-18)
|
||||
- Retroactive review backlog still pending
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-27b — Retroactive review Phase 1 + 2.1, wizard planning
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Retroactive review (morning housekeeping):**
|
||||
- Phase 1.1: Atoms normalize — 3 fixes (Input spacing, Card focus token, unused import)
|
||||
- Phase 1.2: Atom audits — Button 20/20, Input 20/20, Card 18→20/20 (2 P1 fixes: keyboard activation + responsive padding)
|
||||
- Phase 2.1: Molecules normalize — 7 fixes (StepIndicator timing/spacing/font, ProviderCardCompact star colour + icon size)
|
||||
- **Wizard flow documentation review:**
|
||||
- Read flow-spec.md (854 lines), flow-definition.yaml (616 lines), 3 representative step YAMLs
|
||||
- Saved reference memories for flow documentation structure and project context
|
||||
- **Layout analysis:**
|
||||
- Analysed 5 layout reference images in /documentation/layouts/
|
||||
- Identified 5 layout variants: Centered Form, List+Map, List+Detail, Grid+Sidebar, Detail+Toggles
|
||||
- Mapped each layout to specific wizard steps
|
||||
- Saved layout reference to memory
|
||||
- **Implementation plan:**
|
||||
- Created comprehensive plan at .claude/plans/validated-brewing-matsumoto.md
|
||||
- Phase 0 (foundation): WizardLayout template, Collapse atom, ToggleButtonGroup atom
|
||||
- Phases 1–7: Steps in build order with component dependencies
|
||||
- ~11 new components, ~16 sessions estimated
|
||||
- Plan approved by user, updated with layout variants
|
||||
|
||||
**Decisions made:**
|
||||
- Step pages live at `src/components/pages/` (new atomic tier)
|
||||
- CardSelectionGrid extracted after step 3 (not upfront)
|
||||
- Build from specs, Figma for structural shape only
|
||||
- Steps are pure presentation (props in, callbacks out) — no routing/state/GraphQL yet
|
||||
|
||||
**Open questions:**
|
||||
- Hardcoded fontWeight on price typography varies across molecules — still unresolved from normalize
|
||||
|
||||
**Next steps:**
|
||||
- Start Phase 0: Build WizardLayout template (5 variants), Collapse atom, ToggleButtonGroup atom
|
||||
- Then Phase 1: Step 1 (Intro) — first wizard step
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-27b (earlier) — Retroactive review Phase 1 + 2.1
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Phase 1.1 — Atoms normalize:** Scanned all 11 atoms across 7 dimensions. Fixed 3 issues: Input raw px spacing → MUI units, Card focus-visible token → CSS var, removed unused Theme import.
|
||||
- **Phase 1.2 — Atom audits (high priority):** Button 20/20, Input 20/20, Card 18/20 → fixed 2 P1s → 20/20.
|
||||
- Card P1 fix: Added Enter/Space keyboard activation for interactive cards (WCAG 2.1.1)
|
||||
- Card P1 fix: Responsive padding — 16px mobile / 24px desktop (convention alignment)
|
||||
- **Phase 2.1 — Molecules normalize:** Scanned all 8 molecules across 7 dimensions. Fixed 7 issues:
|
||||
- StepIndicator: transition 300ms → 150ms, raw px → MUI spacing, borderRadius → token, mobile font 10px → 11px (D020 floor)
|
||||
- ProviderCardCompact: star colour → warning.main (match ProviderCard), meta icon 16px → 14px (match tier)
|
||||
|
||||
**Decisions made:**
|
||||
- None (all fixes are convention enforcement, no new decisions)
|
||||
|
||||
**Open questions:**
|
||||
- Hardcoded fontWeight on price typography varies across molecules (500 vs 600) — intentional design variation or normalize?
|
||||
- Remaining atom audits (Typography, Badge, Chip, Switch, Radio, IconButton, Divider, Link) — all low/medium priority wrappers, likely 20/20. Run if time permits.
|
||||
|
||||
**Next steps:**
|
||||
- Phase 2.2: Audit priority molecules (ServiceOption, AddOnOption, ProviderCardCompact)
|
||||
- Phase 3: Organisms normalize + audit (Navigation, ServiceSelector, FuneralFinderV3)
|
||||
- Phase 4: Cross-cutting (adapt, typeset, preflight)
|
||||
- New component work as user directs
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-27a — Workflow improvement (5-phase infrastructure upgrade)
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- **Phase 1A:** Archived session log — moved 25 old sessions to `docs/memory/archive/sessions-through-2026-03-26.md`, trimmed active log from 1096 to 91 lines
|
||||
- **Phase 1B:** Documented token access convention as D031 — `theme.palette.*` for semantic tokens, `var(--fa-*)` for component tokens. Fixed Badge.tsx colour map inconsistency (unified to CSS vars). Updated component-conventions.md.
|
||||
- **Phase 2:** Added ESLint v9 (flat config) + Prettier. Installed `eslint`, `@eslint/js`, `typescript-eslint`, `eslint-plugin-react`, `eslint-plugin-react-hooks`, `eslint-plugin-jsx-a11y`, `eslint-config-prettier`, `prettier`. Ran initial format pass across all 54 .tsx files. Fixed 5 empty interface warnings (Switch, Radio, Divider, IconButton, Link). Added `lint`, `lint:fix`, `format`, `format:check` scripts.
|
||||
- **Phase 3:** Created 7 new impeccable-adapted skills: `/polish`, `/harden`, `/normalize`, `/clarify`, `/typeset`, `/quieter`, `/adapt`. Downloaded Vercel reference docs (web-design-guidelines, react-best-practices). Updated `/audit` and `/review-component` with optional Vercel references. Total skills: 19.
|
||||
- **Phase 4:** Added Husky v9 + lint-staged for pre-commit automation. ESLint + Prettier auto-run on staged files. Updated `/preflight` skill with ESLint and Prettier checks (now 8 checks total).
|
||||
- **Phase 5:** Added Vitest v4 + jsdom + @testing-library/react. Created `vitest.config.ts` and test setup. Created `/write-tests` skill for test generation guidance. Added `test` and `test:watch` scripts. Note: `@storybook/test-runner` deferred — requires Storybook 10+ (we have 8).
|
||||
|
||||
**Decisions made:**
|
||||
- D031: Token access convention — theme accessors for semantic, CSS vars for component (see decisions-log)
|
||||
- ESLint story files exempt from `react-hooks/rules-of-hooks` and `no-console` (Storybook render pattern)
|
||||
- Empty `interface extends` changed to `type =` for ESLint compliance (5 wrapper atoms)
|
||||
- Storybook test-runner deferred until Storybook upgrade to v10
|
||||
- Prettier printWidth set to 100 (matching observed code style)
|
||||
|
||||
**Also completed:**
|
||||
- Created `docs/reference/component-lifecycle.md` — 10-stage quality gate sequence (build → QA → polish → present → iterate → normalize → preflight → commit)
|
||||
- Created `docs/reference/retroactive-review-plan.md` — plan to review 30+ existing components (~3.5 sessions)
|
||||
- Updated `/build-atom`, `/build-molecule`, `/build-organism` to include internal QA stages automatically
|
||||
- Added lifecycle reference as CLAUDE.md critical rule #8
|
||||
|
||||
**Open questions:**
|
||||
- Storybook upgrade to v10 — would unlock `@storybook/test-runner` for CI test execution
|
||||
- FuneralFinder version decision (v1/v2/v3) — needed before retroactive review of organisms
|
||||
- Review depth — P0/P1 only (faster) or include P2?
|
||||
|
||||
**Next steps (pick up in next session):**
|
||||
- Start retroactive review: `/normalize atoms` → `/audit` high-priority atoms
|
||||
- Interleave with new component work if preferred
|
||||
- Use `/write-tests` on interactive components as time permits
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-26f — FuneralFinder v3 build + polish
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- Created FuneralFinderV3 — clean vertical form based on user's Figma layout (5919:29445), restyled to FA design system
|
||||
- Two side-by-side StatusCards (Immediate Need default-selected / Pre-planning), stack on mobile
|
||||
- Standard card container (surface.raised, card shadow) — initially built with glassmorphism, user redirected to standard palette
|
||||
- Overline section labels (text.secondary) for "How Can We Help", "Funeral Type", "Location"
|
||||
- Standard outlined fields (white bg, neutral border, brand border on focus, no focus ring per user request)
|
||||
- Location input with LocationOnOutlined pin icon
|
||||
- CTA "Find Funeral Directors →" always active — validates on click, scrolls to first missing field
|
||||
- Dividers after header and before CTA for visual rhythm
|
||||
- Funeral type options: same as V2 + "Show all options"
|
||||
- WAI-ARIA roving tabindex on radiogroup, aria-labelledby via React.useId()
|
||||
- Semantic tokens throughout (border-brand, surface-warm, text-brand, interactive-focus, text-disabled)
|
||||
- Error messages conditionally rendered in aria-live regions (brand copper tone — gentle validation)
|
||||
- First pass scored Critique 33/40, Audit 13/20 → iterated on all findings → re-audit 18/20
|
||||
- Polish pass: revised copy, fixed spacing, defaults, label rename
|
||||
|
||||
**Decisions made:**
|
||||
- Status cards replace V2's step-circle + dropdown — simpler, more visual, side-by-side on desktop
|
||||
- Standard design system styling (user clarified Figma was for structure only, not colour scheme)
|
||||
- "Immediate Need" selected by default — most common use case, status error essentially unreachable
|
||||
- Section renamed from "Current Status" (programmatic) to "How Can We Help" (warm, human)
|
||||
- Copy: "A recent loss or one expected soon" — "expected soon" differentiates from pre-planning gently
|
||||
- Copy: "Planning ahead for yourself or a loved one" — "planning ahead" reinforces non-urgency
|
||||
- No sequential unlock — all fields accessible immediately
|
||||
- CTA always active — validates on click, scrolls to missing field
|
||||
- Form simplified to 3 fields (status, funeral type, location) vs V2's 4
|
||||
- Focus ring suppressed on Select/Input per user requirement — status cards retain focus-visible
|
||||
- Error colour uses text.brand (copper) not feedback.error (red) — intentional for funeral context
|
||||
|
||||
**Open questions:**
|
||||
- Location autocomplete integration still pending across all versions
|
||||
- Decision: V1 vs V2 vs V3 for production
|
||||
|
||||
**Next steps:**
|
||||
- User to review final V3 in Storybook
|
||||
- If V3 chosen: location autocomplete, possible further refinements
|
||||
|
||||
---
|
||||
|
||||
### Session 2026-03-26e — FuneralFinder v2 polish + consistency fixes
|
||||
|
||||
**Agent(s):** Claude Opus 4.6 (1M context)
|
||||
|
||||
**Work completed:**
|
||||
- Fixed location input styling to match select fields (border color, hover, disabled, error states)
|
||||
- Added `active` prop to StepCircle — step 1 uses brand-500 primary fill by default
|
||||
- Aligned Input with full selectSx overrides (bgcolor, disabled opacity+dashed, error border)
|
||||
- Investigated systemic Input vs Select visual differences — confirmed issue is component-local overrides, not theme
|
||||
|
||||
**Decisions made:**
|
||||
- Custom field styling kept within FuneralFinderV2 (intentional divergence from theme for this component's design)
|
||||
- Step 1 circle uses primary fill since it's always active — visual "start here" signal
|
||||
|
||||
**Next steps:**
|
||||
- v2 is feature-complete and polished — ready for user review in Storybook
|
||||
- Decision pending: v1 vs v2 for production
|
||||
- If v2 chosen: add location autocomplete, write flow logic reference doc
|
||||
|
||||
---
|
||||
@@ -1,288 +0,0 @@
|
||||
# Token registry
|
||||
|
||||
Master reference of all design tokens. Updated by the token-architect agent
|
||||
whenever tokens are created or modified. Other agents reference this to find
|
||||
the correct token for any design property.
|
||||
|
||||
## How to use this file
|
||||
|
||||
- **token-architect**: Update this file whenever you create/modify tokens
|
||||
- **component-builder**: Reference this file to find the correct token for any CSS property
|
||||
- **story-writer**: Reference this file to document which tokens a component uses
|
||||
|
||||
## Primitives
|
||||
|
||||
### Colours
|
||||
|
||||
**Brand (warm gold/copper)** — `tokens/primitives/colours.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| color.brand.50 | #FEF9F5 | Lightest warm tint — warm section backgrounds |
|
||||
| color.brand.100 | #F7ECDF | Light warm — hover backgrounds |
|
||||
| color.brand.200 | #EBDAC8 | Warm light — secondary backgrounds |
|
||||
| color.brand.300 | #D8C3B5 | Warm beige (swatch) — surface warmth |
|
||||
| color.brand.400 | #D0A070 | Mid gold (swatch) — secondary interactive |
|
||||
| color.brand.500 | #BA834E | **Base brand** (swatch) — primary CTA |
|
||||
| color.brand.600 | #B0610F | Rich copper (swatch) — hover, brand links |
|
||||
| color.brand.700 | #8B4E0D | Deep copper — active states |
|
||||
| color.brand.800 | #6B3C13 | Dark brown — bold accents |
|
||||
| color.brand.900 | #51301B | Chocolate (swatch) — deep emphasis |
|
||||
| color.brand.950 | #251913 | Espresso (swatch) — darkest brand |
|
||||
|
||||
**Sage (cool grey-green)** — `tokens/primitives/colours.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| color.sage.50 | #F2F5F6 | Lightest sage — cool backgrounds |
|
||||
| color.sage.100 | #E3EAEB | Light sage — hover states |
|
||||
| color.sage.200 | #D7E1E2 | (swatch) — light cool surface |
|
||||
| color.sage.300 | #C8D4D6 | Mid-light sage — dividers |
|
||||
| color.sage.400 | #B9C7C9 | (swatch) — mid sage |
|
||||
| color.sage.500 | #8EA2A7 | Base sage — secondary content |
|
||||
| color.sage.600 | #687D84 | Dark sage — secondary text |
|
||||
| color.sage.700 | #4C5B6B | (swatch) — secondary buttons |
|
||||
| color.sage.800 | #4C5459 | (swatch) — supplementary text |
|
||||
| color.sage.900 | #343C40 | Very dark sage |
|
||||
| color.sage.950 | #1E2528 | Near-black cool |
|
||||
|
||||
**Neutral (true grey)** — `tokens/primitives/colours.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| color.neutral.50-950 | #FAFAFA–#0A0A0B | Full grey scale for text, borders, UI |
|
||||
| color.neutral.800 | #2C2E35 | (swatch) — **Primary text colour** |
|
||||
|
||||
**Feedback hues** — `tokens/primitives/colours.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| color.red.50-950 | #FEF2F2–#3D0E0E | Error/destructive states |
|
||||
| color.amber.50-950 | #FFF9EB–#331F00 | Warning/caution states |
|
||||
| color.green.50-950 | #F0F7F0–#0F2A0F | Success/positive states |
|
||||
| color.blue.50-950 | #EFF6FF–#172554 | Info/informational states |
|
||||
|
||||
**Standalone** — `tokens/primitives/colours.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| color.white | #FFFFFF | Pure white |
|
||||
| color.black | #000000 | Pure black (use sparingly) |
|
||||
|
||||
### Typography
|
||||
|
||||
`tokens/primitives/typography.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| fontFamily.body | Montserrat, ... | Primary body/UI font |
|
||||
| fontFamily.display | Noto Serif SC, ... | Display/heading serif |
|
||||
| fontFamily.mono | JetBrains Mono, ... | Monospace |
|
||||
| fontSize.xs–4xl | 0.75rem–3rem | Desktop font size scale |
|
||||
| fontSize.mobile.display | 1.75rem | 28px mobile display |
|
||||
| fontSize.mobile.h1 | 1.5rem | 24px mobile H1 |
|
||||
| fontSize.mobile.h2 | 1.25rem | 20px mobile H2 |
|
||||
| fontSize.mobile.h3 | 1.125rem | 18px mobile H3 |
|
||||
| fontWeight.regular–bold | 400–700 | Weight scale |
|
||||
| lineHeight.tight–relaxed | 1.25–1.75 | Leading scale |
|
||||
| letterSpacing.tighter–widest | -0.02em–0.08em | Tracking scale |
|
||||
|
||||
### Spacing
|
||||
|
||||
`tokens/primitives/spacing.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| spacing.0-5–20 | 2px–80px | 4px-based spacing scale |
|
||||
| borderRadius.none–full | 0px–9999px | Radius scale |
|
||||
|
||||
### Effects
|
||||
|
||||
`tokens/primitives/effects.json`
|
||||
| Token path | Value | Description |
|
||||
|-----------|-------|-------------|
|
||||
| shadow.sm–xl | CSS box-shadow strings | Elevation shadows |
|
||||
| opacity.disabled | 0.4 | Disabled elements |
|
||||
| opacity.hover | 0.08 | Hover overlay tint |
|
||||
| opacity.overlay | 0.5 | Modal backdrop |
|
||||
|
||||
## Semantic tokens
|
||||
|
||||
### Colour semantics
|
||||
|
||||
`tokens/semantic/colours.json`
|
||||
| Token path | References | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| color.text.primary | → neutral.800 | Main body/heading text |
|
||||
| color.text.secondary | → neutral.600 | Helper text, descriptions |
|
||||
| color.text.tertiary | → neutral.500 | Placeholders, timestamps |
|
||||
| color.text.disabled | → neutral.400 | Disabled text |
|
||||
| color.text.inverse | → white | Text on dark backgrounds |
|
||||
| color.text.brand | → brand.600 | Links, brand emphasis |
|
||||
| color.text.error | → red.600 | Error messages |
|
||||
| color.text.success | → green.600 | Success messages |
|
||||
| color.text.warning | → amber.700 | Warning messages |
|
||||
| color.surface.default | → white | Main page background |
|
||||
| color.surface.subtle | → neutral.50 | Alternate backgrounds |
|
||||
| color.surface.raised | → white | Cards (with shadow) |
|
||||
| color.surface.warm | → brand.50 | Warm-tinted sections |
|
||||
| color.surface.cool | → sage.50 | Cool/calming sections |
|
||||
| color.surface.overlay | #00000080 | Modal backdrop |
|
||||
| color.border.default | → neutral.200 | Standard borders |
|
||||
| color.border.strong | → neutral.400 | Emphasis borders |
|
||||
| color.border.subtle | → neutral.100 | Soft dividers |
|
||||
| color.border.brand | → brand.500 | Focused inputs, brand borders |
|
||||
| color.border.error | → red.500 | Error field borders |
|
||||
| color.border.success | → green.500 | Success field borders |
|
||||
| color.interactive.default | → brand.500 | Primary button, links |
|
||||
| color.interactive.hover | → brand.600 | Hover state |
|
||||
| color.interactive.active | → brand.700 | Active/pressed state |
|
||||
| color.interactive.disabled | → neutral.300 | Disabled interactive |
|
||||
| color.interactive.focus | → brand.500 | Focus ring colour |
|
||||
| color.interactive.secondary | → sage.700 | Secondary buttons |
|
||||
| color.interactive.secondary-hover | → sage.800 | Secondary hover |
|
||||
| color.feedback.success | → green.600 | Success indicator |
|
||||
| color.feedback.success-subtle | → green.50 | Success background |
|
||||
| color.feedback.warning | → amber.600 | Warning indicator |
|
||||
| color.feedback.warning-subtle | → amber.50 | Warning background |
|
||||
| color.feedback.error | → red.600 | Error indicator |
|
||||
| color.feedback.error-subtle | → red.50 | Error background |
|
||||
| color.feedback.info | → blue.600 | Info indicator |
|
||||
| color.feedback.info-subtle | → blue.50 | Info background |
|
||||
|
||||
### Typography semantics
|
||||
|
||||
`tokens/semantic/typography.json`
|
||||
| Token path | References | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| typography.display.* | fontSize.3xl, fontFamily.display, bold, tight | Hero display text |
|
||||
| typography.h1.* | fontSize.2xl, fontFamily.display, bold, tight | Page headings |
|
||||
| typography.h2.* | fontSize.xl, fontFamily.display, semibold, snug | Section headings |
|
||||
| typography.h3.* | fontSize.lg, fontFamily.body, semibold, snug | Sub-headings |
|
||||
| typography.h4.* | fontSize.md, fontFamily.body, semibold, snug | Minor headings |
|
||||
| typography.bodyLarge.* | fontSize.md, fontFamily.body, regular, relaxed | Lead paragraphs |
|
||||
| typography.body.* | fontSize.base, fontFamily.body, regular, normal | Default body |
|
||||
| typography.bodySmall.* | fontSize.sm, fontFamily.body, regular, normal | Helper text |
|
||||
| typography.caption.* | fontSize.xs, fontFamily.body, regular, snug | Fine print |
|
||||
| typography.label.* | fontSize.sm, fontFamily.body, medium, normal | Form labels |
|
||||
| typography.overline.* | fontSize.xs, fontFamily.body, semibold, snug | Section overlines |
|
||||
|
||||
### Spacing semantics
|
||||
|
||||
`tokens/semantic/spacing.json`
|
||||
| Token path | References | Description |
|
||||
|-----------|-----------|-------------|
|
||||
| spacing.component.xs–lg | → spacing.1–6 | Component internal spacing |
|
||||
| spacing.layout.gutter | → spacing.4 | Grid gutter (mobile) |
|
||||
| spacing.layout.gutter-desktop | → spacing.6 | Grid gutter (desktop) |
|
||||
| spacing.layout.section | → spacing.12 | Page section gap |
|
||||
| spacing.layout.page | → spacing.16 | Major section gap |
|
||||
| spacing.layout.page-padding | → spacing.4 | Page horizontal padding (mobile) |
|
||||
| spacing.layout.page-padding-desktop | → spacing.8 | Page horizontal padding (desktop) |
|
||||
|
||||
## Component tokens
|
||||
|
||||
`tokens/component/button.json`
|
||||
|
||||
### Button — Sizing
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| button.height.xs | 28px | Button | Extra-small height — compact text buttons |
|
||||
| button.height.sm | 32px | Button | Small height — secondary actions |
|
||||
| button.height.md | 40px | Button | Medium height — default |
|
||||
| button.height.lg | 48px | Button | Large height — CTAs, mobile (meets 44px touch target) |
|
||||
| button.paddingX.xs–lg | → spacing.2–6 (8–24px) | Button | Horizontal padding per size |
|
||||
| button.paddingY.xs–lg | → spacing.1–3 (4–12px) | Button | Vertical padding per size |
|
||||
| button.fontSize.xs–lg | → fontSize.xs–base (12–16px) | Button | Font size per size |
|
||||
| button.iconSize.xs–lg | 14–20px | Button | Icon dimensions per size |
|
||||
| button.iconGap.xs–lg | → spacing.1–2 (4–8px) | Button | Icon-to-text gap per size |
|
||||
| button.borderRadius.default | → borderRadius.md (8px) | Button | Button corner radius |
|
||||
|
||||
### Card
|
||||
|
||||
`tokens/component/card.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| card.borderRadius.default | → borderRadius.md (8px) | Card | Card corner radius |
|
||||
| card.padding.default | → spacing.6 (24px) | Card | Standard card padding (desktop) |
|
||||
| card.padding.compact | → spacing.4 (16px) | Card | Compact card padding (mobile, tight layouts) |
|
||||
| card.shadow.default | → shadow.md | Card | Resting elevation — medium shadow |
|
||||
| card.shadow.hover | → shadow.lg | Card | Hover elevation — interactive cards |
|
||||
| card.border.default | → color.border.default (#E8E8E8) | Card | Outlined card border colour |
|
||||
| card.border.selected | → color.border.brand (#BA834E) | Card | Brand border for selected/active cards |
|
||||
| card.background.default | → color.surface.raised (#FFFFFF) | Card | Card background (raised surface) |
|
||||
| card.background.hover | → color.surface.subtle (#FAFAFA) | Card | Subtle grey fill on hover for interactive cards |
|
||||
| card.background.selected | → color.surface.warm (#FEF9F5) | Card | Warm tint for selected cards |
|
||||
|
||||
### Badge
|
||||
|
||||
`tokens/component/badge.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| badge.height.sm | 22px | Badge | Small badge height |
|
||||
| badge.height.md | 26px | Badge | Medium badge height (default) |
|
||||
| badge.paddingX.sm | → spacing.2 (8px) | Badge | Small horizontal padding |
|
||||
| badge.paddingX.md | → spacing.3 (12px) | Badge | Medium horizontal padding |
|
||||
| badge.fontSize.sm | → fontSize.2xs (11px) | Badge | Small badge text |
|
||||
| badge.fontSize.md | → fontSize.xs (12px) | Badge | Medium badge text |
|
||||
| badge.iconSize.sm | 12px | Badge | Small badge icon |
|
||||
| badge.iconSize.md | 14px | Badge | Medium badge icon |
|
||||
| badge.iconGap.default | → spacing.1 (4px) | Badge | Icon-to-text gap |
|
||||
| badge.borderRadius.default | → borderRadius.full (9999px) | Badge | Pill shape |
|
||||
|
||||
### Chip
|
||||
|
||||
`tokens/component/chip.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| chip.height.sm | 28px | Chip | Small chip height |
|
||||
| chip.height.md | 32px | Chip | Medium chip height (default) |
|
||||
| chip.paddingX.sm | → spacing.2 (8px) | Chip | Small horizontal padding |
|
||||
| chip.paddingX.md | → spacing.3 (12px) | Chip | Medium horizontal padding |
|
||||
| chip.fontSize.sm | → fontSize.xs (12px) | Chip | Small chip text |
|
||||
| chip.fontSize.md | → fontSize.sm (14px) | Chip | Medium chip text |
|
||||
| chip.iconSize.sm | 16px | Chip | Small chip leading icon |
|
||||
| chip.iconSize.md | 18px | Chip | Medium chip leading icon |
|
||||
| chip.deleteIconSize.sm | 14px | Chip | Small chip delete icon |
|
||||
| chip.deleteIconSize.md | 16px | Chip | Medium chip delete icon |
|
||||
| chip.iconGap.default | → spacing.1 (4px) | Chip | Icon-to-text gap |
|
||||
| chip.borderRadius.default | → borderRadius.full (9999px) | Chip | Pill shape |
|
||||
|
||||
### Switch
|
||||
|
||||
`tokens/component/switch.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| switch.track.width | 44px | Switch | Track width |
|
||||
| switch.track.height | 24px | Switch | Track height |
|
||||
| switch.track.borderRadius | → borderRadius.full (9999px) | Switch | Pill shape |
|
||||
| switch.thumb.size | 18px | Switch | Thumb diameter |
|
||||
|
||||
### Radio
|
||||
|
||||
`tokens/component/radio.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| radio.size.default | 20px | Radio | Outer circle size |
|
||||
| radio.dotSize.default | 10px | Radio | Inner selected dot size |
|
||||
|
||||
### ProviderCard
|
||||
|
||||
`tokens/component/providerCard.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| providerCard.image.height | 180px | ProviderCard | Hero image fixed height |
|
||||
| providerCard.logo.size | 64px | ProviderCard | Logo width/height — rounded rectangle inside image area |
|
||||
| providerCard.logo.borderRadius | → borderRadius.md (8px) | ProviderCard | Rounded rectangle corners |
|
||||
| providerCard.content.padding | → spacing.3 (12px) | ProviderCard | Content area padding |
|
||||
| providerCard.content.gap | → spacing.1 (4px) | ProviderCard | Gap between content rows |
|
||||
|
||||
### VenueCard
|
||||
|
||||
`tokens/component/venueCard.json`
|
||||
|
||||
| Token path | Value / Reference | Used by | Description |
|
||||
|-----------|-----------|---------|-------------|
|
||||
| venueCard.image.height | 180px | VenueCard | Hero image fixed height — matches ProviderCard for list consistency |
|
||||
| venueCard.content.padding | → spacing.3 (12px) | VenueCard | Content area padding |
|
||||
| venueCard.content.gap | → spacing.1 (4px) | VenueCard | Gap between content rows |
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
name: audit
|
||||
description: Run technical quality checks across accessibility, performance, theming, responsive design, and anti-patterns. Generates a scored report with P0-P3 severity ratings and actionable plan. Use when the user wants an accessibility check, performance audit, or technical quality review.
|
||||
user-invocable: true
|
||||
argument-hint: "[area (feature, page, component...)]"
|
||||
---
|
||||
|
||||
## MANDATORY PREPARATION
|
||||
|
||||
Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first.
|
||||
|
||||
---
|
||||
|
||||
Run systematic **technical** quality checks and generate a comprehensive report. Don't fix issues — document them for other commands to address.
|
||||
|
||||
This is a code-level audit, not a design critique. Check what's measurable and verifiable in the implementation.
|
||||
|
||||
## Diagnostic Scan
|
||||
|
||||
Run comprehensive checks across 5 dimensions. Score each dimension 0-4 using the criteria below.
|
||||
|
||||
### 1. Accessibility (A11y)
|
||||
|
||||
**Check for**:
|
||||
- **Contrast issues**: Text contrast ratios < 4.5:1 (or 7:1 for AAA)
|
||||
- **Missing ARIA**: Interactive elements without proper roles, labels, or states
|
||||
- **Keyboard navigation**: Missing focus indicators, illogical tab order, keyboard traps
|
||||
- **Semantic HTML**: Improper heading hierarchy, missing landmarks, divs instead of buttons
|
||||
- **Alt text**: Missing or poor image descriptions
|
||||
- **Form issues**: Inputs without labels, poor error messaging, missing required indicators
|
||||
|
||||
**Score 0-4**: 0=Inaccessible (fails WCAG A), 1=Major gaps (few ARIA labels, no keyboard nav), 2=Partial (some a11y effort, significant gaps), 3=Good (WCAG AA mostly met, minor gaps), 4=Excellent (WCAG AA fully met, approaches AAA)
|
||||
|
||||
### 2. Performance
|
||||
|
||||
**Check for**:
|
||||
- **Layout thrashing**: Reading/writing layout properties in loops
|
||||
- **Expensive animations**: Animating layout properties (width, height, top, left) instead of transform/opacity
|
||||
- **Missing optimization**: Images without lazy loading, unoptimized assets, missing will-change
|
||||
- **Bundle size**: Unnecessary imports, unused dependencies
|
||||
- **Render performance**: Unnecessary re-renders, missing memoization
|
||||
|
||||
**Score 0-4**: 0=Severe issues (layout thrash, unoptimized everything), 1=Major problems (no lazy loading, expensive animations), 2=Partial (some optimization, gaps remain), 3=Good (mostly optimized, minor improvements possible), 4=Excellent (fast, lean, well-optimized)
|
||||
|
||||
### 3. Theming
|
||||
|
||||
**Check for**:
|
||||
- **Hard-coded colors**: Colors not using design tokens
|
||||
- **Broken dark mode**: Missing dark mode variants, poor contrast in dark theme
|
||||
- **Inconsistent tokens**: Using wrong tokens, mixing token types
|
||||
- **Theme switching issues**: Values that don't update on theme change
|
||||
|
||||
**Score 0-4**: 0=No theming (hard-coded everything), 1=Minimal tokens (mostly hard-coded), 2=Partial (tokens exist but inconsistently used), 3=Good (tokens used, minor hard-coded values), 4=Excellent (full token system, dark mode works perfectly)
|
||||
|
||||
### 4. Responsive Design
|
||||
|
||||
**Check for**:
|
||||
- **Fixed widths**: Hard-coded widths that break on mobile
|
||||
- **Touch targets**: Interactive elements < 44x44px
|
||||
- **Horizontal scroll**: Content overflow on narrow viewports
|
||||
- **Text scaling**: Layouts that break when text size increases
|
||||
- **Missing breakpoints**: No mobile/tablet variants
|
||||
|
||||
**Score 0-4**: 0=Desktop-only (breaks on mobile), 1=Major issues (some breakpoints, many failures), 2=Partial (works on mobile, rough edges), 3=Good (responsive, minor touch target or overflow issues), 4=Excellent (fluid, all viewports, proper touch targets)
|
||||
|
||||
### 5. Anti-Patterns (CRITICAL)
|
||||
|
||||
Check against ALL the **DON'T** guidelines in the frontend-design skill. Look for AI slop tells (AI color palette, gradient text, glassmorphism, hero metrics, card grids, generic fonts) and general design anti-patterns (gray on color, nested cards, bounce easing, redundant copy).
|
||||
|
||||
**Score 0-4**: 0=AI slop gallery (5+ tells), 1=Heavy AI aesthetic (3-4 tells), 2=Some tells (1-2 noticeable), 3=Mostly clean (subtle issues only), 4=No AI tells (distinctive, intentional design)
|
||||
|
||||
## Generate Report
|
||||
|
||||
### Audit Health Score
|
||||
|
||||
| # | Dimension | Score | Key Finding |
|
||||
|---|-----------|-------|-------------|
|
||||
| 1 | Accessibility | ? | [most critical a11y issue or "--"] |
|
||||
| 2 | Performance | ? | |
|
||||
| 3 | Responsive Design | ? | |
|
||||
| 4 | Theming | ? | |
|
||||
| 5 | Anti-Patterns | ? | |
|
||||
| **Total** | | **??/20** | **[Rating band]** |
|
||||
|
||||
**Rating bands**: 18-20 Excellent (minor polish), 14-17 Good (address weak dimensions), 10-13 Acceptable (significant work needed), 6-9 Poor (major overhaul), 0-5 Critical (fundamental issues)
|
||||
|
||||
### Anti-Patterns Verdict
|
||||
**Start here.** Pass/fail: Does this look AI-generated? List specific tells. Be brutally honest.
|
||||
|
||||
### Executive Summary
|
||||
- Audit Health Score: **??/20** ([rating band])
|
||||
- Total issues found (count by severity: P0/P1/P2/P3)
|
||||
- Top 3-5 critical issues
|
||||
- Recommended next steps
|
||||
|
||||
### Detailed Findings by Severity
|
||||
|
||||
Tag every issue with **P0-P3 severity**:
|
||||
- **P0 Blocking**: Prevents task completion — fix immediately
|
||||
- **P1 Major**: Significant difficulty or WCAG AA violation — fix before release
|
||||
- **P2 Minor**: Annoyance, workaround exists — fix in next pass
|
||||
- **P3 Polish**: Nice-to-fix, no real user impact — fix if time permits
|
||||
|
||||
For each issue, document:
|
||||
- **[P?] Issue name**
|
||||
- **Location**: Component, file, line
|
||||
- **Category**: Accessibility / Performance / Theming / Responsive / Anti-Pattern
|
||||
- **Impact**: How it affects users
|
||||
- **WCAG/Standard**: Which standard it violates (if applicable)
|
||||
- **Recommendation**: How to fix it
|
||||
- **Suggested command**: Which command to use (prefer: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive)
|
||||
|
||||
### Patterns & Systemic Issues
|
||||
|
||||
Identify recurring problems that indicate systemic gaps rather than one-off mistakes:
|
||||
- "Hard-coded colors appear in 15+ components, should use design tokens"
|
||||
- "Touch targets consistently too small (<44px) throughout mobile experience"
|
||||
|
||||
### Positive Findings
|
||||
|
||||
Note what's working well — good practices to maintain and replicate.
|
||||
|
||||
## Recommended Actions
|
||||
|
||||
List recommended commands in priority order (P0 first, then P1, then P2):
|
||||
|
||||
1. **[P?] `/command-name`** — Brief description (specific context from audit findings)
|
||||
2. **[P?] `/command-name`** — Brief description (specific context)
|
||||
|
||||
**Rules**: Only recommend commands from: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive. Map findings to the most appropriate command. End with `/polish` as the final step if any fixes were recommended.
|
||||
|
||||
After presenting the summary, tell the user:
|
||||
|
||||
> You can ask me to run these one at a time, all at once, or in any order you prefer.
|
||||
>
|
||||
> Re-run `/audit` after fixes to see your score improve.
|
||||
|
||||
**IMPORTANT**: Be thorough but actionable. Too many P3 issues creates noise. Focus on what actually matters.
|
||||
|
||||
**NEVER**:
|
||||
- Report issues without explaining impact (why does this matter?)
|
||||
- Provide generic recommendations (be specific and actionable)
|
||||
- Skip positive findings (celebrate what works)
|
||||
- Forget to prioritize (everything can't be P0)
|
||||
- Report false positives without verification
|
||||
|
||||
Remember: You're a technical quality auditor. Document systematically, prioritize ruthlessly, cite specific code locations, and provide clear paths to improvement.
|
||||
@@ -1,106 +0,0 @@
|
||||
# Cognitive Load Assessment
|
||||
|
||||
Cognitive load is the total mental effort required to use an interface. Overloaded users make mistakes, get frustrated, and leave. This reference helps identify and fix cognitive overload.
|
||||
|
||||
---
|
||||
|
||||
## Three Types of Cognitive Load
|
||||
|
||||
### Intrinsic Load — The Task Itself
|
||||
Complexity inherent to what the user is trying to do. You can't eliminate this, but you can structure it.
|
||||
|
||||
**Manage it by**:
|
||||
- Breaking complex tasks into discrete steps
|
||||
- Providing scaffolding (templates, defaults, examples)
|
||||
- Progressive disclosure — show what's needed now, hide the rest
|
||||
- Grouping related decisions together
|
||||
|
||||
### Extraneous Load — Bad Design
|
||||
Mental effort caused by poor design choices. **Eliminate this ruthlessly** — it's pure waste.
|
||||
|
||||
**Common sources**:
|
||||
- Confusing navigation that requires mental mapping
|
||||
- Unclear labels that force users to guess meaning
|
||||
- Visual clutter competing for attention
|
||||
- Inconsistent patterns that prevent learning
|
||||
- Unnecessary steps between user intent and result
|
||||
|
||||
### Germane Load — Learning Effort
|
||||
Mental effort spent building understanding. This is *good* cognitive load — it leads to mastery.
|
||||
|
||||
**Support it by**:
|
||||
- Progressive disclosure that reveals complexity gradually
|
||||
- Consistent patterns that reward learning
|
||||
- Feedback that confirms correct understanding
|
||||
- Onboarding that teaches through action, not walls of text
|
||||
|
||||
---
|
||||
|
||||
## Cognitive Load Checklist
|
||||
|
||||
Evaluate the interface against these 8 items:
|
||||
|
||||
- [ ] **Single focus**: Can the user complete their primary task without distraction from competing elements?
|
||||
- [ ] **Chunking**: Is information presented in digestible groups (≤4 items per group)?
|
||||
- [ ] **Grouping**: Are related items visually grouped together (proximity, borders, shared background)?
|
||||
- [ ] **Visual hierarchy**: Is it immediately clear what's most important on the screen?
|
||||
- [ ] **One thing at a time**: Can the user focus on a single decision before moving to the next?
|
||||
- [ ] **Minimal choices**: Are decisions simplified (≤4 visible options at any decision point)?
|
||||
- [ ] **Working memory**: Does the user need to remember information from a previous screen to act on the current one?
|
||||
- [ ] **Progressive disclosure**: Is complexity revealed only when the user needs it?
|
||||
|
||||
**Scoring**: Count the failed items. 0–1 failures = low cognitive load (good). 2–3 = moderate (address soon). 4+ = high cognitive load (critical fix needed).
|
||||
|
||||
---
|
||||
|
||||
## The Working Memory Rule
|
||||
|
||||
**Humans can hold ≤4 items in working memory at once** (Miller's Law revised by Cowan, 2001).
|
||||
|
||||
At any decision point, count the number of distinct options, actions, or pieces of information a user must simultaneously consider:
|
||||
- **≤4 items**: Within working memory limits — manageable
|
||||
- **5–7 items**: Pushing the boundary — consider grouping or progressive disclosure
|
||||
- **8+ items**: Overloaded — users will skip, misclick, or abandon
|
||||
|
||||
**Practical applications**:
|
||||
- Navigation menus: ≤5 top-level items (group the rest under clear categories)
|
||||
- Form sections: ≤4 fields visible per group before a visual break
|
||||
- Action buttons: 1 primary, 1–2 secondary, group the rest in a menu
|
||||
- Dashboard widgets: ≤4 key metrics visible without scrolling
|
||||
- Pricing tiers: ≤3 options (more causes analysis paralysis)
|
||||
|
||||
---
|
||||
|
||||
## Common Cognitive Load Violations
|
||||
|
||||
### 1. The Wall of Options
|
||||
**Problem**: Presenting 10+ choices at once with no hierarchy.
|
||||
**Fix**: Group into categories, highlight recommended, use progressive disclosure.
|
||||
|
||||
### 2. The Memory Bridge
|
||||
**Problem**: User must remember info from step 1 to complete step 3.
|
||||
**Fix**: Keep relevant context visible, or repeat it where it's needed.
|
||||
|
||||
### 3. The Hidden Navigation
|
||||
**Problem**: User must build a mental map of where things are.
|
||||
**Fix**: Always show current location (breadcrumbs, active states, progress indicators).
|
||||
|
||||
### 4. The Jargon Barrier
|
||||
**Problem**: Technical or domain language forces translation effort.
|
||||
**Fix**: Use plain language. If domain terms are unavoidable, define them inline.
|
||||
|
||||
### 5. The Visual Noise Floor
|
||||
**Problem**: Every element has the same visual weight — nothing stands out.
|
||||
**Fix**: Establish clear hierarchy: one primary element, 2–3 secondary, everything else muted.
|
||||
|
||||
### 6. The Inconsistent Pattern
|
||||
**Problem**: Similar actions work differently in different places.
|
||||
**Fix**: Standardize interaction patterns. Same type of action = same type of UI.
|
||||
|
||||
### 7. The Multi-Task Demand
|
||||
**Problem**: Interface requires processing multiple simultaneous inputs (reading + deciding + navigating).
|
||||
**Fix**: Sequence the steps. Let the user do one thing at a time.
|
||||
|
||||
### 8. The Context Switch
|
||||
**Problem**: User must jump between screens/tabs/modals to gather info for a single decision.
|
||||
**Fix**: Co-locate the information needed for each decision. Reduce back-and-forth.
|
||||
@@ -1,132 +0,0 @@
|
||||
# Color & Contrast
|
||||
|
||||
## Color Spaces: Use OKLCH
|
||||
|
||||
**Stop using HSL.** Use OKLCH (or LCH) instead. It's perceptually uniform, meaning equal steps in lightness *look* equal—unlike HSL where 50% lightness in yellow looks bright while 50% in blue looks dark.
|
||||
|
||||
```css
|
||||
/* OKLCH: lightness (0-100%), chroma (0-0.4+), hue (0-360) */
|
||||
--color-primary: oklch(60% 0.15 250); /* Blue */
|
||||
--color-primary-light: oklch(85% 0.08 250); /* Same hue, lighter */
|
||||
--color-primary-dark: oklch(35% 0.12 250); /* Same hue, darker */
|
||||
```
|
||||
|
||||
**Key insight**: As you move toward white or black, reduce chroma (saturation). High chroma at extreme lightness looks garish. A light blue at 85% lightness needs ~0.08 chroma, not the 0.15 of your base color.
|
||||
|
||||
## Building Functional Palettes
|
||||
|
||||
### The Tinted Neutral Trap
|
||||
|
||||
**Pure gray is dead.** Add a subtle hint of your brand hue to all neutrals:
|
||||
|
||||
```css
|
||||
/* Dead grays */
|
||||
--gray-100: oklch(95% 0 0); /* No personality */
|
||||
--gray-900: oklch(15% 0 0);
|
||||
|
||||
/* Warm-tinted grays (add brand warmth) */
|
||||
--gray-100: oklch(95% 0.01 60); /* Hint of warmth */
|
||||
--gray-900: oklch(15% 0.01 60);
|
||||
|
||||
/* Cool-tinted grays (tech, professional) */
|
||||
--gray-100: oklch(95% 0.01 250); /* Hint of blue */
|
||||
--gray-900: oklch(15% 0.01 250);
|
||||
```
|
||||
|
||||
The chroma is tiny (0.01) but perceptible. It creates subconscious cohesion between your brand color and your UI.
|
||||
|
||||
### Palette Structure
|
||||
|
||||
A complete system needs:
|
||||
|
||||
| Role | Purpose | Example |
|
||||
|------|---------|---------|
|
||||
| **Primary** | Brand, CTAs, key actions | 1 color, 3-5 shades |
|
||||
| **Neutral** | Text, backgrounds, borders | 9-11 shade scale |
|
||||
| **Semantic** | Success, error, warning, info | 4 colors, 2-3 shades each |
|
||||
| **Surface** | Cards, modals, overlays | 2-3 elevation levels |
|
||||
|
||||
**Skip secondary/tertiary unless you need them.** Most apps work fine with one accent color. Adding more creates decision fatigue and visual noise.
|
||||
|
||||
### The 60-30-10 Rule (Applied Correctly)
|
||||
|
||||
This rule is about **visual weight**, not pixel count:
|
||||
|
||||
- **60%**: Neutral backgrounds, white space, base surfaces
|
||||
- **30%**: Secondary colors—text, borders, inactive states
|
||||
- **10%**: Accent—CTAs, highlights, focus states
|
||||
|
||||
The common mistake: using the accent color everywhere because it's "the brand color." Accent colors work *because* they're rare. Overuse kills their power.
|
||||
|
||||
## Contrast & Accessibility
|
||||
|
||||
### WCAG Requirements
|
||||
|
||||
| Content Type | AA Minimum | AAA Target |
|
||||
|--------------|------------|------------|
|
||||
| Body text | 4.5:1 | 7:1 |
|
||||
| Large text (18px+ or 14px bold) | 3:1 | 4.5:1 |
|
||||
| UI components, icons | 3:1 | 4.5:1 |
|
||||
| Non-essential decorations | None | None |
|
||||
|
||||
**The gotcha**: Placeholder text still needs 4.5:1. That light gray placeholder you see everywhere? Usually fails WCAG.
|
||||
|
||||
### Dangerous Color Combinations
|
||||
|
||||
These commonly fail contrast or cause readability issues:
|
||||
|
||||
- Light gray text on white (the #1 accessibility fail)
|
||||
- **Gray text on any colored background**—gray looks washed out and dead on color. Use a darker shade of the background color, or transparency
|
||||
- Red text on green background (or vice versa)—8% of men can't distinguish these
|
||||
- Blue text on red background (vibrates visually)
|
||||
- Yellow text on white (almost always fails)
|
||||
- Thin light text on images (unpredictable contrast)
|
||||
|
||||
### Never Use Pure Gray or Pure Black
|
||||
|
||||
Pure gray (`oklch(50% 0 0)`) and pure black (`#000`) don't exist in nature—real shadows and surfaces always have a color cast. Even a chroma of 0.005-0.01 is enough to feel natural without being obviously tinted. (See tinted neutrals example above.)
|
||||
|
||||
### Testing
|
||||
|
||||
Don't trust your eyes. Use tools:
|
||||
|
||||
- [WebAIM Contrast Checker](https://webaim.org/resources/contrastchecker/)
|
||||
- Browser DevTools → Rendering → Emulate vision deficiencies
|
||||
- [Polypane](https://polypane.app/) for real-time testing
|
||||
|
||||
## Theming: Light & Dark Mode
|
||||
|
||||
### Dark Mode Is Not Inverted Light Mode
|
||||
|
||||
You can't just swap colors. Dark mode requires different design decisions:
|
||||
|
||||
| Light Mode | Dark Mode |
|
||||
|------------|-----------|
|
||||
| Shadows for depth | Lighter surfaces for depth (no shadows) |
|
||||
| Dark text on light | Light text on dark (reduce font weight) |
|
||||
| Vibrant accents | Desaturate accents slightly |
|
||||
| White backgrounds | Never pure black—use dark gray (oklch 12-18%) |
|
||||
|
||||
```css
|
||||
/* Dark mode depth via surface color, not shadow */
|
||||
:root[data-theme="dark"] {
|
||||
--surface-1: oklch(15% 0.01 250);
|
||||
--surface-2: oklch(20% 0.01 250); /* "Higher" = lighter */
|
||||
--surface-3: oklch(25% 0.01 250);
|
||||
|
||||
/* Reduce text weight slightly */
|
||||
--body-weight: 350; /* Instead of 400 */
|
||||
}
|
||||
```
|
||||
|
||||
### Token Hierarchy
|
||||
|
||||
Use two layers: primitive tokens (`--blue-500`) and semantic tokens (`--color-primary: var(--blue-500)`). For dark mode, only redefine the semantic layer—primitives stay the same.
|
||||
|
||||
## Alpha Is A Design Smell
|
||||
|
||||
Heavy use of transparency (rgba, hsla) usually means an incomplete palette. Alpha creates unpredictable contrast, performance overhead, and inconsistency. Define explicit overlay colors for each context instead. Exception: focus rings and interactive states where see-through is needed.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Relying on color alone to convey information. Creating palettes without clear roles for each color. Using pure black (#000) for large areas. Skipping color blindness testing (8% of men affected).
|
||||
@@ -1,201 +0,0 @@
|
||||
---
|
||||
name: critique
|
||||
description: Evaluate design from a UX perspective, assessing visual hierarchy, information architecture, emotional resonance, cognitive load, and overall quality with quantitative scoring, persona-based testing, and actionable feedback. Use when the user asks to review, critique, evaluate, or give feedback on a design or component.
|
||||
user-invocable: true
|
||||
argument-hint: "[area (feature, page, component...)]"
|
||||
---
|
||||
|
||||
## MANDATORY PREPARATION
|
||||
|
||||
Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: what the interface is trying to accomplish.
|
||||
|
||||
---
|
||||
|
||||
Conduct a holistic design critique, evaluating whether the interface actually works — not just technically, but as a designed experience. Think like a design director giving feedback.
|
||||
|
||||
## Phase 1: Design Critique
|
||||
|
||||
Evaluate the interface across these dimensions:
|
||||
|
||||
### 1. AI Slop Detection (CRITICAL)
|
||||
|
||||
**This is the most important check.** Does this look like every other AI-generated interface from 2024-2025?
|
||||
|
||||
Review the design against ALL the **DON'T** guidelines in the frontend-design skill — they are the fingerprints of AI-generated work. Check for the AI color palette, gradient text, dark mode with glowing accents, glassmorphism, hero metric layouts, identical card grids, generic fonts, and all other tells.
|
||||
|
||||
**The test**: If you showed this to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
|
||||
|
||||
### 2. Visual Hierarchy
|
||||
- Does the eye flow to the most important element first?
|
||||
- Is there a clear primary action? Can you spot it in 2 seconds?
|
||||
- Do size, color, and position communicate importance correctly?
|
||||
- Is there visual competition between elements that should have different weights?
|
||||
|
||||
### 3. Information Architecture & Cognitive Load
|
||||
> *Consult [cognitive-load](reference/cognitive-load.md) for the working memory rule and 8-item checklist*
|
||||
- Is the structure intuitive? Would a new user understand the organization?
|
||||
- Is related content grouped logically?
|
||||
- Are there too many choices at once? Count visible options at each decision point — if >4, flag it
|
||||
- Is the navigation clear and predictable?
|
||||
- **Progressive disclosure**: Is complexity revealed only when needed, or dumped on the user upfront?
|
||||
- **Run the 8-item cognitive load checklist** from the reference. Report failure count: 0–1 = low (good), 2–3 = moderate, 4+ = critical.
|
||||
|
||||
### 4. Emotional Journey
|
||||
- What emotion does this interface evoke? Is that intentional?
|
||||
- Does it match the brand personality?
|
||||
- Does it feel trustworthy, approachable, premium, playful — whatever it should feel?
|
||||
- Would the target user feel "this is for me"?
|
||||
- **Peak-end rule**: Is the most intense moment positive? Does the experience end well (confirmation, celebration, clear next step)?
|
||||
- **Emotional valleys**: Check for onboarding frustration, error cliffs, feature discovery gaps, or anxiety spikes at high-stakes moments (payment, delete, commit)
|
||||
- **Interventions at negative moments**: Are there design interventions where users are likely to feel frustrated or anxious? (progress indicators, reassurance copy, undo options, social proof)
|
||||
|
||||
### 5. Discoverability & Affordance
|
||||
- Are interactive elements obviously interactive?
|
||||
- Would a user know what to do without instructions?
|
||||
- Are hover/focus states providing useful feedback?
|
||||
- Are there hidden features that should be more visible?
|
||||
|
||||
### 6. Composition & Balance
|
||||
- Does the layout feel balanced or uncomfortably weighted?
|
||||
- Is whitespace used intentionally or just leftover?
|
||||
- Is there visual rhythm in spacing and repetition?
|
||||
- Does asymmetry feel designed or accidental?
|
||||
|
||||
### 7. Typography as Communication
|
||||
- Does the type hierarchy clearly signal what to read first, second, third?
|
||||
- Is body text comfortable to read? (line length, spacing, size)
|
||||
- Do font choices reinforce the brand/tone?
|
||||
- Is there enough contrast between heading levels?
|
||||
|
||||
### 8. Color with Purpose
|
||||
- Is color used to communicate, not just decorate?
|
||||
- Does the palette feel cohesive?
|
||||
- Are accent colors drawing attention to the right things?
|
||||
- Does it work for colorblind users? (not just technically — does meaning still come through?)
|
||||
|
||||
### 9. States & Edge Cases
|
||||
- Empty states: Do they guide users toward action, or just say "nothing here"?
|
||||
- Loading states: Do they reduce perceived wait time?
|
||||
- Error states: Are they helpful and non-blaming?
|
||||
- Success states: Do they confirm and guide next steps?
|
||||
|
||||
### 10. Microcopy & Voice
|
||||
- Is the writing clear and concise?
|
||||
- Does it sound like a human (the right human for this brand)?
|
||||
- Are labels and buttons unambiguous?
|
||||
- Does error copy help users fix the problem?
|
||||
|
||||
## Phase 2: Present Findings
|
||||
|
||||
Structure your feedback as a design director would:
|
||||
|
||||
### Design Health Score
|
||||
> *Consult [heuristics-scoring](reference/heuristics-scoring.md)*
|
||||
|
||||
Score each of Nielsen's 10 heuristics 0–4. Present as a table:
|
||||
|
||||
| # | Heuristic | Score | Key Issue |
|
||||
|---|-----------|-------|-----------|
|
||||
| 1 | Visibility of System Status | ? | [specific finding or "—" if solid] |
|
||||
| 2 | Match System / Real World | ? | |
|
||||
| 3 | User Control and Freedom | ? | |
|
||||
| 4 | Consistency and Standards | ? | |
|
||||
| 5 | Error Prevention | ? | |
|
||||
| 6 | Recognition Rather Than Recall | ? | |
|
||||
| 7 | Flexibility and Efficiency | ? | |
|
||||
| 8 | Aesthetic and Minimalist Design | ? | |
|
||||
| 9 | Error Recovery | ? | |
|
||||
| 10 | Help and Documentation | ? | |
|
||||
| **Total** | | **??/40** | **[Rating band]** |
|
||||
|
||||
Be honest with scores. A 4 means genuinely excellent. Most real interfaces score 20–32.
|
||||
|
||||
### Anti-Patterns Verdict
|
||||
**Start here.** Pass/fail: Does this look AI-generated? List specific tells from the skill's Anti-Patterns section. Be brutally honest.
|
||||
|
||||
### Overall Impression
|
||||
A brief gut reaction — what works, what doesn't, and the single biggest opportunity.
|
||||
|
||||
### What's Working
|
||||
Highlight 2–3 things done well. Be specific about why they work.
|
||||
|
||||
### Priority Issues
|
||||
The 3–5 most impactful design problems, ordered by importance.
|
||||
|
||||
For each issue, tag with **P0–P3 severity** (consult [heuristics-scoring](reference/heuristics-scoring.md) for severity definitions):
|
||||
- **[P?] What**: Name the problem clearly
|
||||
- **Why it matters**: How this hurts users or undermines goals
|
||||
- **Fix**: What to do about it (be concrete)
|
||||
- **Suggested command**: Which command could address this (from: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive)
|
||||
|
||||
### Persona Red Flags
|
||||
> *Consult [personas](reference/personas.md)*
|
||||
|
||||
Auto-select 2–3 personas most relevant to this interface type (use the selection table in the reference). If `CLAUDE.md` contains a `## Design Context` section from `teach-impeccable`, also generate 1–2 project-specific personas from the audience/brand info.
|
||||
|
||||
For each selected persona, walk through the primary user action and list specific red flags found:
|
||||
|
||||
**Alex (Power User)**: No keyboard shortcuts detected. Form requires 8 clicks for primary action. Forced modal onboarding. ⚠️ High abandonment risk.
|
||||
|
||||
**Jordan (First-Timer)**: Icon-only nav in sidebar. Technical jargon in error messages ("404 Not Found"). No visible help. ⚠️ Will abandon at step 2.
|
||||
|
||||
Be specific — name the exact elements and interactions that fail each persona. Don't write generic persona descriptions; write what broke for them.
|
||||
|
||||
### Minor Observations
|
||||
Quick notes on smaller issues worth addressing.
|
||||
|
||||
**Remember**:
|
||||
- Be direct — vague feedback wastes everyone's time
|
||||
- Be specific — "the submit button" not "some elements"
|
||||
- Say what's wrong AND why it matters to users
|
||||
- Give concrete suggestions, not just "consider exploring..."
|
||||
- Prioritize ruthlessly — if everything is important, nothing is
|
||||
- Don't soften criticism — developers need honest feedback to ship great design
|
||||
|
||||
## Phase 3: Ask the User
|
||||
|
||||
**After presenting findings**, use targeted questions based on what was actually found. STOP and call the AskUserQuestion tool to clarify. These answers will shape the action plan.
|
||||
|
||||
Ask questions along these lines (adapt to the specific findings — do NOT ask generic questions):
|
||||
|
||||
1. **Priority direction**: Based on the issues found, ask which category matters most to the user right now. For example: "I found problems with visual hierarchy, color usage, and information overload. Which area should we tackle first?" Offer the top 2–3 issue categories as options.
|
||||
|
||||
2. **Design intent**: If the critique found a tonal mismatch, ask whether it was intentional. For example: "The interface feels clinical and corporate. Is that the intended tone, or should it feel warmer/bolder/more playful?" Offer 2–3 tonal directions as options based on what would fix the issues found.
|
||||
|
||||
3. **Scope**: Ask how much the user wants to take on. For example: "I found N issues. Want to address everything, or focus on the top 3?" Offer scope options like "Top 3 only", "All issues", "Critical issues only".
|
||||
|
||||
4. **Constraints** (optional — only ask if relevant): If the findings touch many areas, ask if anything is off-limits. For example: "Should any sections stay as-is?" This prevents the plan from touching things the user considers done.
|
||||
|
||||
**Rules for questions**:
|
||||
- Every question must reference specific findings from Phase 2 — never ask generic "who is your audience?" questions
|
||||
- Keep it to 2–4 questions maximum — respect the user's time
|
||||
- Offer concrete options, not open-ended prompts
|
||||
- If findings are straightforward (e.g., only 1–2 clear issues), skip questions and go directly to Phase 4
|
||||
|
||||
## Phase 4: Recommended Actions
|
||||
|
||||
**After receiving the user's answers**, present a prioritized action summary reflecting the user's priorities and scope from Phase 3.
|
||||
|
||||
### Action Summary
|
||||
|
||||
List recommended commands in priority order, based on the user's answers:
|
||||
|
||||
1. **`/command-name`** — Brief description of what to fix (specific context from critique findings)
|
||||
2. **`/command-name`** — Brief description (specific context)
|
||||
...
|
||||
|
||||
**Rules for recommendations**:
|
||||
- Only recommend commands from: /animate, /quieter, /optimize, /adapt, /clarify, /distill, /delight, /onboard, /normalize, /audit, /harden, /polish, /extract, /bolder, /arrange, /typeset, /critique, /colorize, /overdrive
|
||||
- Order by the user's stated priorities first, then by impact
|
||||
- Each item's description should carry enough context that the command knows what to focus on
|
||||
- Map each Priority Issue to the appropriate command
|
||||
- Skip commands that would address zero issues
|
||||
- If the user chose a limited scope, only include items within that scope
|
||||
- If the user marked areas as off-limits, exclude commands that would touch those areas
|
||||
- End with `/polish` as the final step if any fixes were recommended
|
||||
|
||||
After presenting the summary, tell the user:
|
||||
|
||||
> You can ask me to run these one at a time, all at once, or in any order you prefer.
|
||||
>
|
||||
> Re-run `/critique` after fixes to see your score improve.
|
||||
@@ -1,147 +0,0 @@
|
||||
---
|
||||
name: frontend-design
|
||||
description: Create distinctive, production-grade frontend interfaces with high design quality. Generates creative, polished code that avoids generic AI aesthetics. Use when the user asks to build web components, pages, artifacts, posters, or applications, or when any design skill requires project context.
|
||||
license: Apache 2.0. Based on Anthropic's frontend-design skill. See NOTICE.md for attribution.
|
||||
---
|
||||
|
||||
This skill guides creation of distinctive, production-grade frontend interfaces that avoid generic "AI slop" aesthetics. Implement real working code with exceptional attention to aesthetic details and creative choices.
|
||||
|
||||
## Context Gathering Protocol
|
||||
|
||||
Design skills produce generic output without project context. You MUST have confirmed design context before doing any design work.
|
||||
|
||||
**Required context** — every design skill needs at minimum:
|
||||
- **Target audience**: Who uses this product and in what context?
|
||||
- **Use cases**: What jobs are they trying to get done?
|
||||
- **Brand personality/tone**: How should the interface feel?
|
||||
|
||||
Individual skills may require additional context — check the skill's preparation section for specifics.
|
||||
|
||||
**CRITICAL**: You cannot infer this context by reading the codebase. Code tells you what was built, not who it's for or what it should feel like. Only the creator can provide this context.
|
||||
|
||||
**Gathering order:**
|
||||
1. **Check current instructions (instant)**: If your loaded instructions already contain a **Design Context** section, proceed immediately.
|
||||
2. **Check .impeccable.md (fast)**: If not in instructions, read `.impeccable.md` from the project root. If it exists and contains the required context, proceed.
|
||||
3. **Run teach-impeccable (REQUIRED)**: If neither source has context, you MUST run /teach-impeccable NOW before doing anything else. Do NOT skip this step. Do NOT attempt to infer context from the codebase instead.
|
||||
|
||||
---
|
||||
|
||||
## Design Direction
|
||||
|
||||
Commit to a BOLD aesthetic direction:
|
||||
- **Purpose**: What problem does this interface solve? Who uses it?
|
||||
- **Tone**: Pick an extreme: brutally minimal, maximalist chaos, retro-futuristic, organic/natural, luxury/refined, playful/toy-like, editorial/magazine, brutalist/raw, art deco/geometric, soft/pastel, industrial/utilitarian, etc. There are so many flavors to choose from. Use these for inspiration but design one that is true to the aesthetic direction.
|
||||
- **Constraints**: Technical requirements (framework, performance, accessibility).
|
||||
- **Differentiation**: What makes this UNFORGETTABLE? What's the one thing someone will remember?
|
||||
|
||||
**CRITICAL**: Choose a clear conceptual direction and execute it with precision. Bold maximalism and refined minimalism both work—the key is intentionality, not intensity.
|
||||
|
||||
Then implement working code that is:
|
||||
- Production-grade and functional
|
||||
- Visually striking and memorable
|
||||
- Cohesive with a clear aesthetic point-of-view
|
||||
- Meticulously refined in every detail
|
||||
|
||||
## Frontend Aesthetics Guidelines
|
||||
|
||||
### Typography
|
||||
→ *Consult [typography reference](reference/typography.md) for scales, pairing, and loading strategies.*
|
||||
|
||||
Choose fonts that are beautiful, unique, and interesting. Pair a distinctive display font with a refined body font.
|
||||
|
||||
**DO**: Use a modular type scale with fluid sizing (clamp)
|
||||
**DO**: Vary font weights and sizes to create clear visual hierarchy
|
||||
**DON'T**: Use overused fonts—Inter, Roboto, Arial, Open Sans, system defaults
|
||||
**DON'T**: Use monospace typography as lazy shorthand for "technical/developer" vibes
|
||||
**DON'T**: Put large icons with rounded corners above every heading—they rarely add value and make sites look templated
|
||||
|
||||
### Color & Theme
|
||||
→ *Consult [color reference](reference/color-and-contrast.md) for OKLCH, palettes, and dark mode.*
|
||||
|
||||
Commit to a cohesive palette. Dominant colors with sharp accents outperform timid, evenly-distributed palettes.
|
||||
|
||||
**DO**: Use modern CSS color functions (oklch, color-mix, light-dark) for perceptually uniform, maintainable palettes
|
||||
**DO**: Tint your neutrals toward your brand hue—even a subtle hint creates subconscious cohesion
|
||||
**DON'T**: Use gray text on colored backgrounds—it looks washed out; use a shade of the background color instead
|
||||
**DON'T**: Use pure black (#000) or pure white (#fff)—always tint; pure black/white never appears in nature
|
||||
**DON'T**: Use the AI color palette: cyan-on-dark, purple-to-blue gradients, neon accents on dark backgrounds
|
||||
**DON'T**: Use gradient text for "impact"—especially on metrics or headings; it's decorative rather than meaningful
|
||||
**DON'T**: Default to dark mode with glowing accents—it looks "cool" without requiring actual design decisions
|
||||
|
||||
### Layout & Space
|
||||
→ *Consult [spatial reference](reference/spatial-design.md) for grids, rhythm, and container queries.*
|
||||
|
||||
Create visual rhythm through varied spacing—not the same padding everywhere. Embrace asymmetry and unexpected compositions. Break the grid intentionally for emphasis.
|
||||
|
||||
**DO**: Create visual rhythm through varied spacing—tight groupings, generous separations
|
||||
**DO**: Use fluid spacing with clamp() that breathes on larger screens
|
||||
**DO**: Use asymmetry and unexpected compositions; break the grid intentionally for emphasis
|
||||
**DON'T**: Wrap everything in cards—not everything needs a container
|
||||
**DON'T**: Nest cards inside cards—visual noise, flatten the hierarchy
|
||||
**DON'T**: Use identical card grids—same-sized cards with icon + heading + text, repeated endlessly
|
||||
**DON'T**: Use the hero metric layout template—big number, small label, supporting stats, gradient accent
|
||||
**DON'T**: Center everything—left-aligned text with asymmetric layouts feels more designed
|
||||
**DON'T**: Use the same spacing everywhere—without rhythm, layouts feel monotonous
|
||||
|
||||
### Visual Details
|
||||
**DO**: Use intentional, purposeful decorative elements that reinforce brand
|
||||
**DON'T**: Use glassmorphism everywhere—blur effects, glass cards, glow borders used decoratively rather than purposefully
|
||||
**DON'T**: Use rounded elements with thick colored border on one side—a lazy accent that almost never looks intentional
|
||||
**DON'T**: Use sparklines as decoration—tiny charts that look sophisticated but convey nothing meaningful
|
||||
**DON'T**: Use rounded rectangles with generic drop shadows—safe, forgettable, could be any AI output
|
||||
**DON'T**: Use modals unless there's truly no better alternative—modals are lazy
|
||||
|
||||
### Motion
|
||||
→ *Consult [motion reference](reference/motion-design.md) for timing, easing, and reduced motion.*
|
||||
|
||||
Focus on high-impact moments: one well-orchestrated page load with staggered reveals creates more delight than scattered micro-interactions.
|
||||
|
||||
**DO**: Use motion to convey state changes—entrances, exits, feedback
|
||||
**DO**: Use exponential easing (ease-out-quart/quint/expo) for natural deceleration
|
||||
**DO**: For height animations, use grid-template-rows transitions instead of animating height directly
|
||||
**DON'T**: Animate layout properties (width, height, padding, margin)—use transform and opacity only
|
||||
**DON'T**: Use bounce or elastic easing—they feel dated and tacky; real objects decelerate smoothly
|
||||
|
||||
### Interaction
|
||||
→ *Consult [interaction reference](reference/interaction-design.md) for forms, focus, and loading patterns.*
|
||||
|
||||
Make interactions feel fast. Use optimistic UI—update immediately, sync later.
|
||||
|
||||
**DO**: Use progressive disclosure—start simple, reveal sophistication through interaction (basic options first, advanced behind expandable sections; hover states that reveal secondary actions)
|
||||
**DO**: Design empty states that teach the interface, not just say "nothing here"
|
||||
**DO**: Make every interactive surface feel intentional and responsive
|
||||
**DON'T**: Repeat the same information—redundant headers, intros that restate the heading
|
||||
**DON'T**: Make every button primary—use ghost buttons, text links, secondary styles; hierarchy matters
|
||||
|
||||
### Responsive
|
||||
→ *Consult [responsive reference](reference/responsive-design.md) for mobile-first, fluid design, and container queries.*
|
||||
|
||||
**DO**: Use container queries (@container) for component-level responsiveness
|
||||
**DO**: Adapt the interface for different contexts—don't just shrink it
|
||||
**DON'T**: Hide critical functionality on mobile—adapt the interface, don't amputate it
|
||||
|
||||
### UX Writing
|
||||
→ *Consult [ux-writing reference](reference/ux-writing.md) for labels, errors, and empty states.*
|
||||
|
||||
**DO**: Make every word earn its place
|
||||
**DON'T**: Repeat information users can already see
|
||||
|
||||
---
|
||||
|
||||
## The AI Slop Test
|
||||
|
||||
**Critical quality check**: If you showed this interface to someone and said "AI made this," would they believe you immediately? If yes, that's the problem.
|
||||
|
||||
A distinctive interface should make someone ask "how was this made?" not "which AI made this?"
|
||||
|
||||
Review the DON'T guidelines above—they are the fingerprints of AI-generated work from 2024-2025.
|
||||
|
||||
---
|
||||
|
||||
## Implementation Principles
|
||||
|
||||
Match implementation complexity to the aesthetic vision. Maximalist designs need elaborate code with extensive animations and effects. Minimalist or refined designs need restraint, precision, and careful attention to spacing, typography, and subtle details.
|
||||
|
||||
Interpret creatively and make unexpected choices that feel genuinely designed for the context. No design should be the same. Vary between light and dark themes, different fonts, different aesthetics. NEVER converge on common choices across generations.
|
||||
|
||||
Remember: Claude is capable of extraordinary creative work. Don't hold back—show what can truly be created when thinking outside the box and committing fully to a distinctive vision.
|
||||
@@ -1,234 +0,0 @@
|
||||
# Heuristics Scoring Guide
|
||||
|
||||
Score each of Nielsen's 10 Usability Heuristics on a 0–4 scale. Be honest — a 4 means genuinely excellent, not "good enough."
|
||||
|
||||
## Nielsen's 10 Heuristics
|
||||
|
||||
### 1. Visibility of System Status
|
||||
|
||||
Keep users informed about what's happening through timely, appropriate feedback.
|
||||
|
||||
**Check for**:
|
||||
- Loading indicators during async operations
|
||||
- Confirmation of user actions (save, submit, delete)
|
||||
- Progress indicators for multi-step processes
|
||||
- Current location in navigation (breadcrumbs, active states)
|
||||
- Form validation feedback (inline, not just on submit)
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | No feedback — user is guessing what happened |
|
||||
| 1 | Rare feedback — most actions produce no visible response |
|
||||
| 2 | Partial — some states communicated, major gaps remain |
|
||||
| 3 | Good — most operations give clear feedback, minor gaps |
|
||||
| 4 | Excellent — every action confirms, progress is always visible |
|
||||
|
||||
### 2. Match Between System and Real World
|
||||
|
||||
Speak the user's language. Follow real-world conventions. Information appears in natural, logical order.
|
||||
|
||||
**Check for**:
|
||||
- Familiar terminology (no unexplained jargon)
|
||||
- Logical information order matching user expectations
|
||||
- Recognizable icons and metaphors
|
||||
- Domain-appropriate language for the target audience
|
||||
- Natural reading flow (left-to-right, top-to-bottom priority)
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Pure tech jargon, alien to users |
|
||||
| 1 | Mostly confusing — requires domain expertise to navigate |
|
||||
| 2 | Mixed — some plain language, some jargon leaks through |
|
||||
| 3 | Mostly natural — occasional term needs context |
|
||||
| 4 | Speaks the user's language fluently throughout |
|
||||
|
||||
### 3. User Control and Freedom
|
||||
|
||||
Users need a clear "emergency exit" from unwanted states without extended dialogue.
|
||||
|
||||
**Check for**:
|
||||
- Undo/redo functionality
|
||||
- Cancel buttons on forms and modals
|
||||
- Clear navigation back to safety (home, previous)
|
||||
- Easy way to clear filters, search, selections
|
||||
- Escape from long or multi-step processes
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Users get trapped — no way out without refreshing |
|
||||
| 1 | Difficult exits — must find obscure paths to escape |
|
||||
| 2 | Some exits — main flows have escape, edge cases don't |
|
||||
| 3 | Good control — users can exit and undo most actions |
|
||||
| 4 | Full control — undo, cancel, back, and escape everywhere |
|
||||
|
||||
### 4. Consistency and Standards
|
||||
|
||||
Users shouldn't wonder whether different words, situations, or actions mean the same thing.
|
||||
|
||||
**Check for**:
|
||||
- Consistent terminology throughout the interface
|
||||
- Same actions produce same results everywhere
|
||||
- Platform conventions followed (standard UI patterns)
|
||||
- Visual consistency (colors, typography, spacing, components)
|
||||
- Consistent interaction patterns (same gesture = same behavior)
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Inconsistent everywhere — feels like different products stitched together |
|
||||
| 1 | Many inconsistencies — similar things look/behave differently |
|
||||
| 2 | Partially consistent — main flows match, details diverge |
|
||||
| 3 | Mostly consistent — occasional deviation, nothing confusing |
|
||||
| 4 | Fully consistent — cohesive system, predictable behavior |
|
||||
|
||||
### 5. Error Prevention
|
||||
|
||||
Better than good error messages is a design that prevents problems in the first place.
|
||||
|
||||
**Check for**:
|
||||
- Confirmation before destructive actions (delete, overwrite)
|
||||
- Constraints preventing invalid input (date pickers, dropdowns)
|
||||
- Smart defaults that reduce errors
|
||||
- Clear labels that prevent misunderstanding
|
||||
- Autosave and draft recovery
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Errors easy to make — no guardrails anywhere |
|
||||
| 1 | Few safeguards — some inputs validated, most aren't |
|
||||
| 2 | Partial prevention — common errors caught, edge cases slip |
|
||||
| 3 | Good prevention — most error paths blocked proactively |
|
||||
| 4 | Excellent — errors nearly impossible through smart constraints |
|
||||
|
||||
### 6. Recognition Rather Than Recall
|
||||
|
||||
Minimize memory load. Make objects, actions, and options visible or easily retrievable.
|
||||
|
||||
**Check for**:
|
||||
- Visible options (not buried in hidden menus)
|
||||
- Contextual help when needed (tooltips, inline hints)
|
||||
- Recent items and history
|
||||
- Autocomplete and suggestions
|
||||
- Labels on icons (not icon-only navigation)
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Heavy memorization — users must remember paths and commands |
|
||||
| 1 | Mostly recall — many hidden features, few visible cues |
|
||||
| 2 | Some aids — main actions visible, secondary features hidden |
|
||||
| 3 | Good recognition — most things discoverable, few memory demands |
|
||||
| 4 | Everything discoverable — users never need to memorize |
|
||||
|
||||
### 7. Flexibility and Efficiency of Use
|
||||
|
||||
Accelerators — invisible to novices — speed up expert interaction.
|
||||
|
||||
**Check for**:
|
||||
- Keyboard shortcuts for common actions
|
||||
- Customizable interface elements
|
||||
- Recent items and favorites
|
||||
- Bulk/batch actions
|
||||
- Power user features that don't complicate the basics
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | One rigid path — no shortcuts or alternatives |
|
||||
| 1 | Limited flexibility — few alternatives to the main path |
|
||||
| 2 | Some shortcuts — basic keyboard support, limited bulk actions |
|
||||
| 3 | Good accelerators — keyboard nav, some customization |
|
||||
| 4 | Highly flexible — multiple paths, power features, customizable |
|
||||
|
||||
### 8. Aesthetic and Minimalist Design
|
||||
|
||||
Interfaces should not contain irrelevant or rarely needed information. Every element should serve a purpose.
|
||||
|
||||
**Check for**:
|
||||
- Only necessary information visible at each step
|
||||
- Clear visual hierarchy directing attention
|
||||
- Purposeful use of color and emphasis
|
||||
- No decorative clutter competing for attention
|
||||
- Focused, uncluttered layouts
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Overwhelming — everything competes for attention equally |
|
||||
| 1 | Cluttered — too much noise, hard to find what matters |
|
||||
| 2 | Some clutter — main content clear, periphery noisy |
|
||||
| 3 | Mostly clean — focused design, minor visual noise |
|
||||
| 4 | Perfectly minimal — every element earns its pixel |
|
||||
|
||||
### 9. Help Users Recognize, Diagnose, and Recover from Errors
|
||||
|
||||
Error messages should use plain language, precisely indicate the problem, and constructively suggest a solution.
|
||||
|
||||
**Check for**:
|
||||
- Plain language error messages (no error codes for users)
|
||||
- Specific problem identification ("Email is missing @" not "Invalid input")
|
||||
- Actionable recovery suggestions
|
||||
- Errors displayed near the source of the problem
|
||||
- Non-blocking error handling (don't wipe the form)
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | Cryptic errors — codes, jargon, or no message at all |
|
||||
| 1 | Vague errors — "Something went wrong" with no guidance |
|
||||
| 2 | Clear but unhelpful — names the problem but not the fix |
|
||||
| 3 | Clear with suggestions — identifies problem and offers next steps |
|
||||
| 4 | Perfect recovery — pinpoints issue, suggests fix, preserves user work |
|
||||
|
||||
### 10. Help and Documentation
|
||||
|
||||
Even if the system is usable without docs, help should be easy to find, task-focused, and concise.
|
||||
|
||||
**Check for**:
|
||||
- Searchable help or documentation
|
||||
- Contextual help (tooltips, inline hints, guided tours)
|
||||
- Task-focused organization (not feature-organized)
|
||||
- Concise, scannable content
|
||||
- Easy access without leaving current context
|
||||
|
||||
**Scoring**:
|
||||
| Score | Criteria |
|
||||
|-------|----------|
|
||||
| 0 | No help available anywhere |
|
||||
| 1 | Help exists but hard to find or irrelevant |
|
||||
| 2 | Basic help — FAQ or docs exist, not contextual |
|
||||
| 3 | Good documentation — searchable, mostly task-focused |
|
||||
| 4 | Excellent contextual help — right info at the right moment |
|
||||
|
||||
---
|
||||
|
||||
## Score Summary
|
||||
|
||||
**Total possible**: 40 points (10 heuristics × 4 max)
|
||||
|
||||
| Score Range | Rating | What It Means |
|
||||
|-------------|--------|---------------|
|
||||
| 36–40 | Excellent | Minor polish only — ship it |
|
||||
| 28–35 | Good | Address weak areas, solid foundation |
|
||||
| 20–27 | Acceptable | Significant improvements needed before users are happy |
|
||||
| 12–19 | Poor | Major UX overhaul required — core experience broken |
|
||||
| 0–11 | Critical | Redesign needed — unusable in current state |
|
||||
|
||||
---
|
||||
|
||||
## Issue Severity (P0–P3)
|
||||
|
||||
Tag each individual issue found during scoring with a priority level:
|
||||
|
||||
| Priority | Name | Description | Action |
|
||||
|----------|------|-------------|--------|
|
||||
| **P0** | Blocking | Prevents task completion entirely | Fix immediately — this is a showstopper |
|
||||
| **P1** | Major | Causes significant difficulty or confusion | Fix before release |
|
||||
| **P2** | Minor | Annoyance, but workaround exists | Fix in next pass |
|
||||
| **P3** | Polish | Nice-to-fix, no real user impact | Fix if time permits |
|
||||
|
||||
**Tip**: If you're unsure between two levels, ask: "Would a user contact support about this?" If yes, it's at least P1.
|
||||
@@ -1,195 +0,0 @@
|
||||
# Interaction Design
|
||||
|
||||
## The Eight Interactive States
|
||||
|
||||
Every interactive element needs these states designed:
|
||||
|
||||
| State | When | Visual Treatment |
|
||||
|-------|------|------------------|
|
||||
| **Default** | At rest | Base styling |
|
||||
| **Hover** | Pointer over (not touch) | Subtle lift, color shift |
|
||||
| **Focus** | Keyboard/programmatic focus | Visible ring (see below) |
|
||||
| **Active** | Being pressed | Pressed in, darker |
|
||||
| **Disabled** | Not interactive | Reduced opacity, no pointer |
|
||||
| **Loading** | Processing | Spinner, skeleton |
|
||||
| **Error** | Invalid state | Red border, icon, message |
|
||||
| **Success** | Completed | Green check, confirmation |
|
||||
|
||||
**The common miss**: Designing hover without focus, or vice versa. They're different. Keyboard users never see hover states.
|
||||
|
||||
## Focus Rings: Do Them Right
|
||||
|
||||
**Never `outline: none` without replacement.** It's an accessibility violation. Instead, use `:focus-visible` to show focus only for keyboard users:
|
||||
|
||||
```css
|
||||
/* Hide focus ring for mouse/touch */
|
||||
button:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Show focus ring for keyboard */
|
||||
button:focus-visible {
|
||||
outline: 2px solid var(--color-accent);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
```
|
||||
|
||||
**Focus ring design**:
|
||||
- High contrast (3:1 minimum against adjacent colors)
|
||||
- 2-3px thick
|
||||
- Offset from element (not inside it)
|
||||
- Consistent across all interactive elements
|
||||
|
||||
## Form Design: The Non-Obvious
|
||||
|
||||
**Placeholders aren't labels**—they disappear on input. Always use visible `<label>` elements. **Validate on blur**, not on every keystroke (exception: password strength). Place errors **below** fields with `aria-describedby` connecting them.
|
||||
|
||||
## Loading States
|
||||
|
||||
**Optimistic updates**: Show success immediately, rollback on failure. Use for low-stakes actions (likes, follows), not payments or destructive actions. **Skeleton screens > spinners**—they preview content shape and feel faster than generic spinners.
|
||||
|
||||
## Modals: The Inert Approach
|
||||
|
||||
Focus trapping in modals used to require complex JavaScript. Now use the `inert` attribute:
|
||||
|
||||
```html
|
||||
<!-- When modal is open -->
|
||||
<main inert>
|
||||
<!-- Content behind modal can't be focused or clicked -->
|
||||
</main>
|
||||
<dialog open>
|
||||
<h2>Modal Title</h2>
|
||||
<!-- Focus stays inside modal -->
|
||||
</dialog>
|
||||
```
|
||||
|
||||
Or use the native `<dialog>` element:
|
||||
|
||||
```javascript
|
||||
const dialog = document.querySelector('dialog');
|
||||
dialog.showModal(); // Opens with focus trap, closes on Escape
|
||||
```
|
||||
|
||||
## The Popover API
|
||||
|
||||
For tooltips, dropdowns, and non-modal overlays, use native popovers:
|
||||
|
||||
```html
|
||||
<button popovertarget="menu">Open menu</button>
|
||||
<div id="menu" popover>
|
||||
<button>Option 1</button>
|
||||
<button>Option 2</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
**Benefits**: Light-dismiss (click outside closes), proper stacking, no z-index wars, accessible by default.
|
||||
|
||||
## Dropdown & Overlay Positioning
|
||||
|
||||
Dropdowns rendered with `position: absolute` inside a container that has `overflow: hidden` or `overflow: auto` will be clipped. This is the single most common dropdown bug in generated code.
|
||||
|
||||
### CSS Anchor Positioning
|
||||
|
||||
The modern solution uses the CSS Anchor Positioning API to tether an overlay to its trigger without JavaScript:
|
||||
|
||||
```css
|
||||
.trigger {
|
||||
anchor-name: --menu-trigger;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
position-anchor: --menu-trigger;
|
||||
position-area: block-end span-inline-end;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Flip above if no room below */
|
||||
@position-try --flip-above {
|
||||
position-area: block-start span-inline-end;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
```
|
||||
|
||||
Because the dropdown uses `position: fixed`, it escapes any `overflow` clipping on ancestor elements. The `@position-try` block handles viewport edges automatically. **Browser support**: Chrome 125+, Edge 125+. Not yet in Firefox or Safari - use a fallback for those browsers.
|
||||
|
||||
### Popover + Anchor Combo
|
||||
|
||||
Combining the Popover API with anchor positioning gives you stacking, light-dismiss, accessibility, and correct positioning in one pattern:
|
||||
|
||||
```html
|
||||
<button popovertarget="menu" class="trigger">Open</button>
|
||||
<div id="menu" popover class="dropdown">
|
||||
<button>Option 1</button>
|
||||
<button>Option 2</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
The `popover` attribute places the element in the **top layer**, which sits above all other content regardless of z-index or overflow. No portal needed.
|
||||
|
||||
### Portal / Teleport Pattern
|
||||
|
||||
In component frameworks, render the dropdown at the document root and position it with JavaScript:
|
||||
|
||||
- **React**: `createPortal(dropdown, document.body)`
|
||||
- **Vue**: `<Teleport to="body">`
|
||||
- **Svelte**: Use a portal library or mount to `document.body`
|
||||
|
||||
Calculate position from the trigger's `getBoundingClientRect()`, then apply `position: fixed` with `top` and `left` values. Recalculate on scroll and resize.
|
||||
|
||||
### Fixed Positioning Fallback
|
||||
|
||||
For browsers without anchor positioning support, `position: fixed` with manual coordinates avoids overflow clipping:
|
||||
|
||||
```css
|
||||
.dropdown {
|
||||
position: fixed;
|
||||
/* top/left set via JS from trigger's getBoundingClientRect() */
|
||||
}
|
||||
```
|
||||
|
||||
Check viewport boundaries before rendering. If the dropdown would overflow the bottom edge, flip it above the trigger. If it would overflow the right edge, align it to the trigger's right side instead.
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
- **`position: absolute` inside `overflow: hidden`** - The dropdown will be clipped. Use `position: fixed` or the top layer instead.
|
||||
- **Arbitrary z-index values** like `z-index: 9999` - Use a semantic z-index scale: `dropdown (100) -> sticky (200) -> modal-backdrop (300) -> modal (400) -> toast (500) -> tooltip (600)`.
|
||||
- **Rendering dropdown markup inline** without an escape hatch from the parent's stacking context. Either use `popover` (top layer), a portal, or `position: fixed`.
|
||||
|
||||
## Destructive Actions: Undo > Confirm
|
||||
|
||||
**Undo is better than confirmation dialogs**—users click through confirmations mindlessly. Remove from UI immediately, show undo toast, actually delete after toast expires. Use confirmation only for truly irreversible actions (account deletion), high-cost actions, or batch operations.
|
||||
|
||||
## Keyboard Navigation Patterns
|
||||
|
||||
### Roving Tabindex
|
||||
|
||||
For component groups (tabs, menu items, radio groups), one item is tabbable; arrow keys move within:
|
||||
|
||||
```html
|
||||
<div role="tablist">
|
||||
<button role="tab" tabindex="0">Tab 1</button>
|
||||
<button role="tab" tabindex="-1">Tab 2</button>
|
||||
<button role="tab" tabindex="-1">Tab 3</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
Arrow keys move `tabindex="0"` between items. Tab moves to the next component entirely.
|
||||
|
||||
### Skip Links
|
||||
|
||||
Provide skip links (`<a href="#main-content">Skip to main content</a>`) for keyboard users to jump past navigation. Hide off-screen, show on focus.
|
||||
|
||||
## Gesture Discoverability
|
||||
|
||||
Swipe-to-delete and similar gestures are invisible. Hint at their existence:
|
||||
|
||||
- **Partially reveal**: Show delete button peeking from edge
|
||||
- **Onboarding**: Coach marks on first use
|
||||
- **Alternative**: Always provide a visible fallback (menu with "Delete")
|
||||
|
||||
Don't rely on gestures as the only way to perform actions.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Removing focus indicators without alternatives. Using placeholder text as labels. Touch targets <44x44px. Generic error messages. Custom controls without ARIA/keyboard support.
|
||||
@@ -1,99 +0,0 @@
|
||||
# Motion Design
|
||||
|
||||
## Duration: The 100/300/500 Rule
|
||||
|
||||
Timing matters more than easing. These durations feel right for most UI:
|
||||
|
||||
| Duration | Use Case | Examples |
|
||||
|----------|----------|----------|
|
||||
| **100-150ms** | Instant feedback | Button press, toggle, color change |
|
||||
| **200-300ms** | State changes | Menu open, tooltip, hover states |
|
||||
| **300-500ms** | Layout changes | Accordion, modal, drawer |
|
||||
| **500-800ms** | Entrance animations | Page load, hero reveals |
|
||||
|
||||
**Exit animations are faster than entrances**—use ~75% of enter duration.
|
||||
|
||||
## Easing: Pick the Right Curve
|
||||
|
||||
**Don't use `ease`.** It's a compromise that's rarely optimal. Instead:
|
||||
|
||||
| Curve | Use For | CSS |
|
||||
|-------|---------|-----|
|
||||
| **ease-out** | Elements entering | `cubic-bezier(0.16, 1, 0.3, 1)` |
|
||||
| **ease-in** | Elements leaving | `cubic-bezier(0.7, 0, 0.84, 0)` |
|
||||
| **ease-in-out** | State toggles (there → back) | `cubic-bezier(0.65, 0, 0.35, 1)` |
|
||||
|
||||
**For micro-interactions, use exponential curves**—they feel natural because they mimic real physics (friction, deceleration):
|
||||
|
||||
```css
|
||||
/* Quart out - smooth, refined (recommended default) */
|
||||
--ease-out-quart: cubic-bezier(0.25, 1, 0.5, 1);
|
||||
|
||||
/* Quint out - slightly more dramatic */
|
||||
--ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
|
||||
|
||||
/* Expo out - snappy, confident */
|
||||
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
```
|
||||
|
||||
**Avoid bounce and elastic curves.** They were trendy in 2015 but now feel tacky and amateurish. Real objects don't bounce when they stop—they decelerate smoothly. Overshoot effects draw attention to the animation itself rather than the content.
|
||||
|
||||
## The Only Two Properties You Should Animate
|
||||
|
||||
**transform** and **opacity** only—everything else causes layout recalculation. For height animations (accordions), use `grid-template-rows: 0fr → 1fr` instead of animating `height` directly.
|
||||
|
||||
## Staggered Animations
|
||||
|
||||
Use CSS custom properties for cleaner stagger: `animation-delay: calc(var(--i, 0) * 50ms)` with `style="--i: 0"` on each item. **Cap total stagger time**—10 items at 50ms = 500ms total. For many items, reduce per-item delay or cap staggered count.
|
||||
|
||||
## Reduced Motion
|
||||
|
||||
This is not optional. Vestibular disorders affect ~35% of adults over 40.
|
||||
|
||||
```css
|
||||
/* Define animations normally */
|
||||
.card {
|
||||
animation: slide-up 500ms ease-out;
|
||||
}
|
||||
|
||||
/* Provide alternative for reduced motion */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.card {
|
||||
animation: fade-in 200ms ease-out; /* Crossfade instead of motion */
|
||||
}
|
||||
}
|
||||
|
||||
/* Or disable entirely */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: 0.01ms !important;
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**What to preserve**: Functional animations like progress bars, loading spinners (slowed down), and focus indicators should still work—just without spatial movement.
|
||||
|
||||
## Perceived Performance
|
||||
|
||||
**Nobody cares how fast your site is—just how fast it feels.** Perception can be as effective as actual performance.
|
||||
|
||||
**The 80ms threshold**: Our brains buffer sensory input for ~80ms to synchronize perception. Anything under 80ms feels instant and simultaneous. This is your target for micro-interactions.
|
||||
|
||||
**Active vs passive time**: Passive waiting (staring at a spinner) feels longer than active engagement. Strategies to shift the balance:
|
||||
|
||||
- **Preemptive start**: Begin transitions immediately while loading (iOS app zoom, skeleton UI). Users perceive work happening.
|
||||
- **Early completion**: Show content progressively—don't wait for everything. Video buffering, progressive images, streaming HTML.
|
||||
- **Optimistic UI**: Update the interface immediately, handle failures gracefully. Instagram likes work offline—the UI updates instantly, syncs later. Use for low-stakes actions; avoid for payments or destructive operations.
|
||||
|
||||
**Easing affects perceived duration**: Ease-in (accelerating toward completion) makes tasks feel shorter because the peak-end effect weights final moments heavily. Ease-out feels satisfying for entrances, but ease-in toward a task's end compresses perceived time.
|
||||
|
||||
**Caution**: Too-fast responses can decrease perceived value. Users may distrust instant results for complex operations (search, analysis). Sometimes a brief delay signals "real work" is happening.
|
||||
|
||||
## Performance
|
||||
|
||||
Don't use `will-change` preemptively—only when animation is imminent (`:hover`, `.animating`). For scroll-triggered animations, use Intersection Observer instead of scroll events; unobserve after animating once. Create motion tokens for consistency (durations, easings, common transitions).
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Animating everything (animation fatigue is real). Using >500ms for UI feedback. Ignoring `prefers-reduced-motion`. Using animation to hide slow loading.
|
||||
@@ -1,178 +0,0 @@
|
||||
# Persona-Based Design Testing
|
||||
|
||||
Test the interface through the eyes of 5 distinct user archetypes. Each persona exposes different failure modes that a single "design director" perspective would miss.
|
||||
|
||||
**How to use**: Select 2–3 personas most relevant to the interface being critiqued. Walk through the primary user action as each persona. Report specific red flags — not generic concerns.
|
||||
|
||||
---
|
||||
|
||||
## 1. Impatient Power User — "Alex"
|
||||
|
||||
**Profile**: Expert with similar products. Expects efficiency, hates hand-holding. Will find shortcuts or leave.
|
||||
|
||||
**Behaviors**:
|
||||
- Skips all onboarding and instructions
|
||||
- Looks for keyboard shortcuts immediately
|
||||
- Tries to bulk-select, batch-edit, and automate
|
||||
- Gets frustrated by required steps that feel unnecessary
|
||||
- Abandons if anything feels slow or patronizing
|
||||
|
||||
**Test Questions**:
|
||||
- Can Alex complete the core task in under 60 seconds?
|
||||
- Are there keyboard shortcuts for common actions?
|
||||
- Can onboarding be skipped entirely?
|
||||
- Do modals have keyboard dismiss (Esc)?
|
||||
- Is there a "power user" path (shortcuts, bulk actions)?
|
||||
|
||||
**Red Flags** (report these specifically):
|
||||
- Forced tutorials or unskippable onboarding
|
||||
- No keyboard navigation for primary actions
|
||||
- Slow animations that can't be skipped
|
||||
- One-item-at-a-time workflows where batch would be natural
|
||||
- Redundant confirmation steps for low-risk actions
|
||||
|
||||
---
|
||||
|
||||
## 2. Confused First-Timer — "Jordan"
|
||||
|
||||
**Profile**: Never used this type of product. Needs guidance at every step. Will abandon rather than figure it out.
|
||||
|
||||
**Behaviors**:
|
||||
- Reads all instructions carefully
|
||||
- Hesitates before clicking anything unfamiliar
|
||||
- Looks for help or support constantly
|
||||
- Misunderstands jargon and abbreviations
|
||||
- Takes the most literal interpretation of any label
|
||||
|
||||
**Test Questions**:
|
||||
- Is the first action obviously clear within 5 seconds?
|
||||
- Are all icons labeled with text?
|
||||
- Is there contextual help at decision points?
|
||||
- Does terminology assume prior knowledge?
|
||||
- Is there a clear "back" or "undo" at every step?
|
||||
|
||||
**Red Flags** (report these specifically):
|
||||
- Icon-only navigation with no labels
|
||||
- Technical jargon without explanation
|
||||
- No visible help option or guidance
|
||||
- Ambiguous next steps after completing an action
|
||||
- No confirmation that an action succeeded
|
||||
|
||||
---
|
||||
|
||||
## 3. Accessibility-Dependent User — "Sam"
|
||||
|
||||
**Profile**: Uses screen reader (VoiceOver/NVDA), keyboard-only navigation. May have low vision, motor impairment, or cognitive differences.
|
||||
|
||||
**Behaviors**:
|
||||
- Tabs through the interface linearly
|
||||
- Relies on ARIA labels and heading structure
|
||||
- Cannot see hover states or visual-only indicators
|
||||
- Needs adequate color contrast (4.5:1 minimum)
|
||||
- May use browser zoom up to 200%
|
||||
|
||||
**Test Questions**:
|
||||
- Can the entire primary flow be completed keyboard-only?
|
||||
- Are all interactive elements focusable with visible focus indicators?
|
||||
- Do images have meaningful alt text?
|
||||
- Is color contrast WCAG AA compliant (4.5:1 for text)?
|
||||
- Does the screen reader announce state changes (loading, success, errors)?
|
||||
|
||||
**Red Flags** (report these specifically):
|
||||
- Click-only interactions with no keyboard alternative
|
||||
- Missing or invisible focus indicators
|
||||
- Meaning conveyed by color alone (red = error, green = success)
|
||||
- Unlabeled form fields or buttons
|
||||
- Time-limited actions without extension option
|
||||
- Custom components that break screen reader flow
|
||||
|
||||
---
|
||||
|
||||
## 4. Deliberate Stress Tester — "Riley"
|
||||
|
||||
**Profile**: Methodical user who pushes interfaces beyond the happy path. Tests edge cases, tries unexpected inputs, and probes for gaps in the experience.
|
||||
|
||||
**Behaviors**:
|
||||
- Tests edge cases intentionally (empty states, long strings, special characters)
|
||||
- Submits forms with unexpected data (emoji, RTL text, very long values)
|
||||
- Tries to break workflows by navigating backwards, refreshing mid-flow, or opening in multiple tabs
|
||||
- Looks for inconsistencies between what the UI promises and what actually happens
|
||||
- Documents problems methodically
|
||||
|
||||
**Test Questions**:
|
||||
- What happens at the edges (0 items, 1000 items, very long text)?
|
||||
- Do error states recover gracefully or leave the UI in a broken state?
|
||||
- What happens on refresh mid-workflow? Is state preserved?
|
||||
- Are there features that appear to work but produce broken results?
|
||||
- How does the UI handle unexpected input (emoji, special chars, paste from Excel)?
|
||||
|
||||
**Red Flags** (report these specifically):
|
||||
- Features that appear to work but silently fail or produce wrong results
|
||||
- Error handling that exposes technical details or leaves UI in a broken state
|
||||
- Empty states that show nothing useful ("No results" with no guidance)
|
||||
- Workflows that lose user data on refresh or navigation
|
||||
- Inconsistent behavior between similar interactions in different parts of the UI
|
||||
|
||||
---
|
||||
|
||||
## 5. Distracted Mobile User — "Casey"
|
||||
|
||||
**Profile**: Using phone one-handed on the go. Frequently interrupted. Possibly on a slow connection.
|
||||
|
||||
**Behaviors**:
|
||||
- Uses thumb only — prefers bottom-of-screen actions
|
||||
- Gets interrupted mid-flow and returns later
|
||||
- Switches between apps frequently
|
||||
- Has limited attention span and low patience
|
||||
- Types as little as possible, prefers taps and selections
|
||||
|
||||
**Test Questions**:
|
||||
- Are primary actions in the thumb zone (bottom half of screen)?
|
||||
- Is state preserved if the user leaves and returns?
|
||||
- Does it work on slow connections (3G)?
|
||||
- Can forms leverage autocomplete and smart defaults?
|
||||
- Are touch targets at least 44×44pt?
|
||||
|
||||
**Red Flags** (report these specifically):
|
||||
- Important actions positioned at the top of the screen (unreachable by thumb)
|
||||
- No state persistence — progress lost on tab switch or interruption
|
||||
- Large text inputs required where selection would work
|
||||
- Heavy assets loading on every page (no lazy loading)
|
||||
- Tiny tap targets or targets too close together
|
||||
|
||||
---
|
||||
|
||||
## Selecting Personas
|
||||
|
||||
Choose personas based on the interface type:
|
||||
|
||||
| Interface Type | Primary Personas | Why |
|
||||
|---------------|-----------------|-----|
|
||||
| Landing page / marketing | Jordan, Riley, Casey | First impressions, trust, mobile |
|
||||
| Dashboard / admin | Alex, Sam | Power users, accessibility |
|
||||
| E-commerce / checkout | Casey, Riley, Jordan | Mobile, edge cases, clarity |
|
||||
| Onboarding flow | Jordan, Casey | Confusion, interruption |
|
||||
| Data-heavy / analytics | Alex, Sam | Efficiency, keyboard nav |
|
||||
| Form-heavy / wizard | Jordan, Sam, Casey | Clarity, accessibility, mobile |
|
||||
|
||||
---
|
||||
|
||||
## Project-Specific Personas
|
||||
|
||||
If `CLAUDE.md` contains a `## Design Context` section (generated by `teach-impeccable`), derive 1–2 additional personas from the audience and brand information:
|
||||
|
||||
1. Read the target audience description
|
||||
2. Identify the primary user archetype not covered by the 5 predefined personas
|
||||
3. Create a persona following this template:
|
||||
|
||||
```
|
||||
### [Role] — "[Name]"
|
||||
|
||||
**Profile**: [2-3 key characteristics derived from Design Context]
|
||||
|
||||
**Behaviors**: [3-4 specific behaviors based on the described audience]
|
||||
|
||||
**Red Flags**: [3-4 things that would alienate this specific user type]
|
||||
```
|
||||
|
||||
Only generate project-specific personas when real Design Context data is available. Don't invent audience details — use the 5 predefined personas when no context exists.
|
||||
@@ -1,202 +0,0 @@
|
||||
---
|
||||
name: polish
|
||||
description: Performs a final quality pass fixing alignment, spacing, consistency, and micro-detail issues before shipping. Use when the user mentions polish, finishing touches, pre-launch review, something looks off, or wants to go from good to great.
|
||||
user-invocable: true
|
||||
argument-hint: "[target]"
|
||||
---
|
||||
|
||||
## MANDATORY PREPARATION
|
||||
|
||||
Invoke /frontend-design — it contains design principles, anti-patterns, and the **Context Gathering Protocol**. Follow the protocol before proceeding — if no design context exists yet, you MUST run /teach-impeccable first. Additionally gather: quality bar (MVP vs flagship).
|
||||
|
||||
---
|
||||
|
||||
Perform a meticulous final pass to catch all the small details that separate good work from great work. The difference between shipped and polished.
|
||||
|
||||
## Pre-Polish Assessment
|
||||
|
||||
Understand the current state and goals:
|
||||
|
||||
1. **Review completeness**:
|
||||
- Is it functionally complete?
|
||||
- Are there known issues to preserve (mark with TODOs)?
|
||||
- What's the quality bar? (MVP vs flagship feature?)
|
||||
- When does it ship? (How much time for polish?)
|
||||
|
||||
2. **Identify polish areas**:
|
||||
- Visual inconsistencies
|
||||
- Spacing and alignment issues
|
||||
- Interaction state gaps
|
||||
- Copy inconsistencies
|
||||
- Edge cases and error states
|
||||
- Loading and transition smoothness
|
||||
|
||||
**CRITICAL**: Polish is the last step, not the first. Don't polish work that's not functionally complete.
|
||||
|
||||
## Polish Systematically
|
||||
|
||||
Work through these dimensions methodically:
|
||||
|
||||
### Visual Alignment & Spacing
|
||||
|
||||
- **Pixel-perfect alignment**: Everything lines up to grid
|
||||
- **Consistent spacing**: All gaps use spacing scale (no random 13px gaps)
|
||||
- **Optical alignment**: Adjust for visual weight (icons may need offset for optical centering)
|
||||
- **Responsive consistency**: Spacing and alignment work at all breakpoints
|
||||
- **Grid adherence**: Elements snap to baseline grid
|
||||
|
||||
**Check**:
|
||||
- Enable grid overlay and verify alignment
|
||||
- Check spacing with browser inspector
|
||||
- Test at multiple viewport sizes
|
||||
- Look for elements that "feel" off
|
||||
|
||||
### Typography Refinement
|
||||
|
||||
- **Hierarchy consistency**: Same elements use same sizes/weights throughout
|
||||
- **Line length**: 45-75 characters for body text
|
||||
- **Line height**: Appropriate for font size and context
|
||||
- **Widows & orphans**: No single words on last line
|
||||
- **Hyphenation**: Appropriate for language and column width
|
||||
- **Kerning**: Adjust letter spacing where needed (especially headlines)
|
||||
- **Font loading**: No FOUT/FOIT flashes
|
||||
|
||||
### Color & Contrast
|
||||
|
||||
- **Contrast ratios**: All text meets WCAG standards
|
||||
- **Consistent token usage**: No hard-coded colors, all use design tokens
|
||||
- **Theme consistency**: Works in all theme variants
|
||||
- **Color meaning**: Same colors mean same things throughout
|
||||
- **Accessible focus**: Focus indicators visible with sufficient contrast
|
||||
- **Tinted neutrals**: No pure gray or pure black—add subtle color tint (0.01 chroma)
|
||||
- **Gray on color**: Never put gray text on colored backgrounds—use a shade of that color or transparency
|
||||
|
||||
### Interaction States
|
||||
|
||||
Every interactive element needs all states:
|
||||
|
||||
- **Default**: Resting state
|
||||
- **Hover**: Subtle feedback (color, scale, shadow)
|
||||
- **Focus**: Keyboard focus indicator (never remove without replacement)
|
||||
- **Active**: Click/tap feedback
|
||||
- **Disabled**: Clearly non-interactive
|
||||
- **Loading**: Async action feedback
|
||||
- **Error**: Validation or error state
|
||||
- **Success**: Successful completion
|
||||
|
||||
**Missing states create confusion and broken experiences**.
|
||||
|
||||
### Micro-interactions & Transitions
|
||||
|
||||
- **Smooth transitions**: All state changes animated appropriately (150-300ms)
|
||||
- **Consistent easing**: Use ease-out-quart/quint/expo for natural deceleration. Never bounce or elastic—they feel dated.
|
||||
- **No jank**: 60fps animations, only animate transform and opacity
|
||||
- **Appropriate motion**: Motion serves purpose, not decoration
|
||||
- **Reduced motion**: Respects `prefers-reduced-motion`
|
||||
|
||||
### Content & Copy
|
||||
|
||||
- **Consistent terminology**: Same things called same names throughout
|
||||
- **Consistent capitalization**: Title Case vs Sentence case applied consistently
|
||||
- **Grammar & spelling**: No typos
|
||||
- **Appropriate length**: Not too wordy, not too terse
|
||||
- **Punctuation consistency**: Periods on sentences, not on labels (unless all labels have them)
|
||||
|
||||
### Icons & Images
|
||||
|
||||
- **Consistent style**: All icons from same family or matching style
|
||||
- **Appropriate sizing**: Icons sized consistently for context
|
||||
- **Proper alignment**: Icons align with adjacent text optically
|
||||
- **Alt text**: All images have descriptive alt text
|
||||
- **Loading states**: Images don't cause layout shift, proper aspect ratios
|
||||
- **Retina support**: 2x assets for high-DPI screens
|
||||
|
||||
### Forms & Inputs
|
||||
|
||||
- **Label consistency**: All inputs properly labeled
|
||||
- **Required indicators**: Clear and consistent
|
||||
- **Error messages**: Helpful and consistent
|
||||
- **Tab order**: Logical keyboard navigation
|
||||
- **Auto-focus**: Appropriate (don't overuse)
|
||||
- **Validation timing**: Consistent (on blur vs on submit)
|
||||
|
||||
### Edge Cases & Error States
|
||||
|
||||
- **Loading states**: All async actions have loading feedback
|
||||
- **Empty states**: Helpful empty states, not just blank space
|
||||
- **Error states**: Clear error messages with recovery paths
|
||||
- **Success states**: Confirmation of successful actions
|
||||
- **Long content**: Handles very long names, descriptions, etc.
|
||||
- **No content**: Handles missing data gracefully
|
||||
- **Offline**: Appropriate offline handling (if applicable)
|
||||
|
||||
### Responsiveness
|
||||
|
||||
- **All breakpoints**: Test mobile, tablet, desktop
|
||||
- **Touch targets**: 44x44px minimum on touch devices
|
||||
- **Readable text**: No text smaller than 14px on mobile
|
||||
- **No horizontal scroll**: Content fits viewport
|
||||
- **Appropriate reflow**: Content adapts logically
|
||||
|
||||
### Performance
|
||||
|
||||
- **Fast initial load**: Optimize critical path
|
||||
- **No layout shift**: Elements don't jump after load (CLS)
|
||||
- **Smooth interactions**: No lag or jank
|
||||
- **Optimized images**: Appropriate formats and sizes
|
||||
- **Lazy loading**: Off-screen content loads lazily
|
||||
|
||||
### Code Quality
|
||||
|
||||
- **Remove console logs**: No debug logging in production
|
||||
- **Remove commented code**: Clean up dead code
|
||||
- **Remove unused imports**: Clean up unused dependencies
|
||||
- **Consistent naming**: Variables and functions follow conventions
|
||||
- **Type safety**: No TypeScript `any` or ignored errors
|
||||
- **Accessibility**: Proper ARIA labels and semantic HTML
|
||||
|
||||
## Polish Checklist
|
||||
|
||||
Go through systematically:
|
||||
|
||||
- [ ] Visual alignment perfect at all breakpoints
|
||||
- [ ] Spacing uses design tokens consistently
|
||||
- [ ] Typography hierarchy consistent
|
||||
- [ ] All interactive states implemented
|
||||
- [ ] All transitions smooth (60fps)
|
||||
- [ ] Copy is consistent and polished
|
||||
- [ ] Icons are consistent and properly sized
|
||||
- [ ] All forms properly labeled and validated
|
||||
- [ ] Error states are helpful
|
||||
- [ ] Loading states are clear
|
||||
- [ ] Empty states are welcoming
|
||||
- [ ] Touch targets are 44x44px minimum
|
||||
- [ ] Contrast ratios meet WCAG AA
|
||||
- [ ] Keyboard navigation works
|
||||
- [ ] Focus indicators visible
|
||||
- [ ] No console errors or warnings
|
||||
- [ ] No layout shift on load
|
||||
- [ ] Works in all supported browsers
|
||||
- [ ] Respects reduced motion preference
|
||||
- [ ] Code is clean (no TODOs, console.logs, commented code)
|
||||
|
||||
**IMPORTANT**: Polish is about details. Zoom in. Squint at it. Use it yourself. The little things add up.
|
||||
|
||||
**NEVER**:
|
||||
- Polish before it's functionally complete
|
||||
- Spend hours on polish if it ships in 30 minutes (triage)
|
||||
- Introduce bugs while polishing (test thoroughly)
|
||||
- Ignore systematic issues (if spacing is off everywhere, fix the system)
|
||||
- Perfect one thing while leaving others rough (consistent quality level)
|
||||
|
||||
## Final Verification
|
||||
|
||||
Before marking as done:
|
||||
|
||||
- **Use it yourself**: Actually interact with the feature
|
||||
- **Test on real devices**: Not just browser DevTools
|
||||
- **Ask someone else to review**: Fresh eyes catch things
|
||||
- **Compare to design**: Match intended design
|
||||
- **Check all states**: Don't just test happy path
|
||||
|
||||
Remember: You have impeccable attention to detail and exquisite taste. Polish until it feels effortless, looks intentional, and works flawlessly. Sweat the details - they matter.
|
||||
@@ -1,114 +0,0 @@
|
||||
# Responsive Design
|
||||
|
||||
## Mobile-First: Write It Right
|
||||
|
||||
Start with base styles for mobile, use `min-width` queries to layer complexity. Desktop-first (`max-width`) means mobile loads unnecessary styles first.
|
||||
|
||||
## Breakpoints: Content-Driven
|
||||
|
||||
Don't chase device sizes—let content tell you where to break. Start narrow, stretch until design breaks, add breakpoint there. Three breakpoints usually suffice (640, 768, 1024px). Use `clamp()` for fluid values without breakpoints.
|
||||
|
||||
## Detect Input Method, Not Just Screen Size
|
||||
|
||||
**Screen size doesn't tell you input method.** A laptop with touchscreen, a tablet with keyboard—use pointer and hover queries:
|
||||
|
||||
```css
|
||||
/* Fine pointer (mouse, trackpad) */
|
||||
@media (pointer: fine) {
|
||||
.button { padding: 8px 16px; }
|
||||
}
|
||||
|
||||
/* Coarse pointer (touch, stylus) */
|
||||
@media (pointer: coarse) {
|
||||
.button { padding: 12px 20px; } /* Larger touch target */
|
||||
}
|
||||
|
||||
/* Device supports hover */
|
||||
@media (hover: hover) {
|
||||
.card:hover { transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
/* Device doesn't support hover (touch) */
|
||||
@media (hover: none) {
|
||||
.card { /* No hover state - use active instead */ }
|
||||
}
|
||||
```
|
||||
|
||||
**Critical**: Don't rely on hover for functionality. Touch users can't hover.
|
||||
|
||||
## Safe Areas: Handle the Notch
|
||||
|
||||
Modern phones have notches, rounded corners, and home indicators. Use `env()`:
|
||||
|
||||
```css
|
||||
body {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
padding-right: env(safe-area-inset-right);
|
||||
}
|
||||
|
||||
/* With fallback */
|
||||
.footer {
|
||||
padding-bottom: max(1rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
```
|
||||
|
||||
**Enable viewport-fit** in your meta tag:
|
||||
```html
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||
```
|
||||
|
||||
## Responsive Images: Get It Right
|
||||
|
||||
### srcset with Width Descriptors
|
||||
|
||||
```html
|
||||
<img
|
||||
src="hero-800.jpg"
|
||||
srcset="
|
||||
hero-400.jpg 400w,
|
||||
hero-800.jpg 800w,
|
||||
hero-1200.jpg 1200w
|
||||
"
|
||||
sizes="(max-width: 768px) 100vw, 50vw"
|
||||
alt="Hero image"
|
||||
>
|
||||
```
|
||||
|
||||
**How it works**:
|
||||
- `srcset` lists available images with their actual widths (`w` descriptors)
|
||||
- `sizes` tells the browser how wide the image will display
|
||||
- Browser picks the best file based on viewport width AND device pixel ratio
|
||||
|
||||
### Picture Element for Art Direction
|
||||
|
||||
When you need different crops/compositions (not just resolutions):
|
||||
|
||||
```html
|
||||
<picture>
|
||||
<source media="(min-width: 768px)" srcset="wide.jpg">
|
||||
<source media="(max-width: 767px)" srcset="tall.jpg">
|
||||
<img src="fallback.jpg" alt="...">
|
||||
</picture>
|
||||
```
|
||||
|
||||
## Layout Adaptation Patterns
|
||||
|
||||
**Navigation**: Three stages—hamburger + drawer on mobile, horizontal compact on tablet, full with labels on desktop. **Tables**: Transform to cards on mobile using `display: block` and `data-label` attributes. **Progressive disclosure**: Use `<details>/<summary>` for content that can collapse on mobile.
|
||||
|
||||
## Testing: Don't Trust DevTools Alone
|
||||
|
||||
DevTools device emulation is useful for layout but misses:
|
||||
|
||||
- Actual touch interactions
|
||||
- Real CPU/memory constraints
|
||||
- Network latency patterns
|
||||
- Font rendering differences
|
||||
- Browser chrome/keyboard appearances
|
||||
|
||||
**Test on at least**: One real iPhone, one real Android, a tablet if relevant. Cheap Android phones reveal performance issues you'll never see on simulators.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Desktop-first design. Device detection instead of feature detection. Separate mobile/desktop codebases. Ignoring tablet and landscape. Assuming all mobile devices are powerful.
|
||||
@@ -1,100 +0,0 @@
|
||||
# Spatial Design
|
||||
|
||||
## Spacing Systems
|
||||
|
||||
### Use 4pt Base, Not 8pt
|
||||
|
||||
8pt systems are too coarse—you'll frequently need 12px (between 8 and 16). Use 4pt for granularity: 4, 8, 12, 16, 24, 32, 48, 64, 96px.
|
||||
|
||||
### Name Tokens Semantically
|
||||
|
||||
Name by relationship (`--space-sm`, `--space-lg`), not value (`--spacing-8`). Use `gap` instead of margins for sibling spacing—it eliminates margin collapse and cleanup hacks.
|
||||
|
||||
## Grid Systems
|
||||
|
||||
### The Self-Adjusting Grid
|
||||
|
||||
Use `repeat(auto-fit, minmax(280px, 1fr))` for responsive grids without breakpoints. Columns are at least 280px, as many as fit per row, leftovers stretch. For complex layouts, use named grid areas (`grid-template-areas`) and redefine them at breakpoints.
|
||||
|
||||
## Visual Hierarchy
|
||||
|
||||
### The Squint Test
|
||||
|
||||
Blur your eyes (or screenshot and blur). Can you still identify:
|
||||
- The most important element?
|
||||
- The second most important?
|
||||
- Clear groupings?
|
||||
|
||||
If everything looks the same weight blurred, you have a hierarchy problem.
|
||||
|
||||
### Hierarchy Through Multiple Dimensions
|
||||
|
||||
Don't rely on size alone. Combine:
|
||||
|
||||
| Tool | Strong Hierarchy | Weak Hierarchy |
|
||||
|------|------------------|----------------|
|
||||
| **Size** | 3:1 ratio or more | <2:1 ratio |
|
||||
| **Weight** | Bold vs Regular | Medium vs Regular |
|
||||
| **Color** | High contrast | Similar tones |
|
||||
| **Position** | Top/left (primary) | Bottom/right |
|
||||
| **Space** | Surrounded by white space | Crowded |
|
||||
|
||||
**The best hierarchy uses 2-3 dimensions at once**: A heading that's larger, bolder, AND has more space above it.
|
||||
|
||||
### Cards Are Not Required
|
||||
|
||||
Cards are overused. Spacing and alignment create visual grouping naturally. Use cards only when content is truly distinct and actionable, items need visual comparison in a grid, or content needs clear interaction boundaries. **Never nest cards inside cards**—use spacing, typography, and subtle dividers for hierarchy within a card.
|
||||
|
||||
## Container Queries
|
||||
|
||||
Viewport queries are for page layouts. **Container queries are for components**:
|
||||
|
||||
```css
|
||||
.card-container {
|
||||
container-type: inline-size;
|
||||
}
|
||||
|
||||
.card {
|
||||
display: grid;
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
/* Card layout changes based on its container, not viewport */
|
||||
@container (min-width: 400px) {
|
||||
.card {
|
||||
grid-template-columns: 120px 1fr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Why this matters**: A card in a narrow sidebar stays compact, while the same card in a main content area expands—automatically, without viewport hacks.
|
||||
|
||||
## Optical Adjustments
|
||||
|
||||
Text at `margin-left: 0` looks indented due to letterform whitespace—use negative margin (`-0.05em`) to optically align. Geometrically centered icons often look off-center; play icons need to shift right, arrows shift toward their direction.
|
||||
|
||||
### Touch Targets vs Visual Size
|
||||
|
||||
Buttons can look small but need large touch targets (44px minimum). Use padding or pseudo-elements:
|
||||
|
||||
```css
|
||||
.icon-button {
|
||||
width: 24px; /* Visual size */
|
||||
height: 24px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.icon-button::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: -10px; /* Expand tap target to 44px */
|
||||
}
|
||||
```
|
||||
|
||||
## Depth & Elevation
|
||||
|
||||
Create semantic z-index scales (dropdown → sticky → modal-backdrop → modal → toast → tooltip) instead of arbitrary numbers. For shadows, create a consistent elevation scale (sm → md → lg → xl). **Key insight**: Shadows should be subtle—if you can clearly see it, it's probably too strong.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Arbitrary spacing values outside your scale. Making all spacing equal (variety creates hierarchy). Creating hierarchy through size alone - combine size, weight, color, and space.
|
||||
@@ -1,133 +0,0 @@
|
||||
# Typography
|
||||
|
||||
## Classic Typography Principles
|
||||
|
||||
### Vertical Rhythm
|
||||
|
||||
Your line-height should be the base unit for ALL vertical spacing. If body text has `line-height: 1.5` on `16px` type (= 24px), spacing values should be multiples of 24px. This creates subconscious harmony—text and space share a mathematical foundation.
|
||||
|
||||
### Modular Scale & Hierarchy
|
||||
|
||||
The common mistake: too many font sizes that are too close together (14px, 15px, 16px, 18px...). This creates muddy hierarchy.
|
||||
|
||||
**Use fewer sizes with more contrast.** A 5-size system covers most needs:
|
||||
|
||||
| Role | Typical Ratio | Use Case |
|
||||
|------|---------------|----------|
|
||||
| xs | 0.75rem | Captions, legal |
|
||||
| sm | 0.875rem | Secondary UI, metadata |
|
||||
| base | 1rem | Body text |
|
||||
| lg | 1.25-1.5rem | Subheadings, lead text |
|
||||
| xl+ | 2-4rem | Headlines, hero text |
|
||||
|
||||
Popular ratios: 1.25 (major third), 1.333 (perfect fourth), 1.5 (perfect fifth). Pick one and commit.
|
||||
|
||||
### Readability & Measure
|
||||
|
||||
Use `ch` units for character-based measure (`max-width: 65ch`). Line-height scales inversely with line length—narrow columns need tighter leading, wide columns need more.
|
||||
|
||||
**Non-obvious**: Increase line-height for light text on dark backgrounds. The perceived weight is lighter, so text needs more breathing room. Add 0.05-0.1 to your normal line-height.
|
||||
|
||||
## Font Selection & Pairing
|
||||
|
||||
### Choosing Distinctive Fonts
|
||||
|
||||
**Avoid the invisible defaults**: Inter, Roboto, Open Sans, Lato, Montserrat. These are everywhere, making your design feel generic. They're fine for documentation or tools where personality isn't the goal—but if you want distinctive design, look elsewhere.
|
||||
|
||||
**Better Google Fonts alternatives**:
|
||||
- Instead of Inter → **Instrument Sans**, **Plus Jakarta Sans**, **Outfit**
|
||||
- Instead of Roboto → **Onest**, **Figtree**, **Urbanist**
|
||||
- Instead of Open Sans → **Source Sans 3**, **Nunito Sans**, **DM Sans**
|
||||
- For editorial/premium feel → **Fraunces**, **Newsreader**, **Lora**
|
||||
|
||||
**System fonts are underrated**: `-apple-system, BlinkMacSystemFont, "Segoe UI", system-ui` looks native, loads instantly, and is highly readable. Consider this for apps where performance > personality.
|
||||
|
||||
### Pairing Principles
|
||||
|
||||
**The non-obvious truth**: You often don't need a second font. One well-chosen font family in multiple weights creates cleaner hierarchy than two competing typefaces. Only add a second font when you need genuine contrast (e.g., display headlines + body serif).
|
||||
|
||||
When pairing, contrast on multiple axes:
|
||||
- Serif + Sans (structure contrast)
|
||||
- Geometric + Humanist (personality contrast)
|
||||
- Condensed display + Wide body (proportion contrast)
|
||||
|
||||
**Never pair fonts that are similar but not identical** (e.g., two geometric sans-serifs). They create visual tension without clear hierarchy.
|
||||
|
||||
### Web Font Loading
|
||||
|
||||
The layout shift problem: fonts load late, text reflows, and users see content jump. Here's the fix:
|
||||
|
||||
```css
|
||||
/* 1. Use font-display: swap for visibility */
|
||||
@font-face {
|
||||
font-family: 'CustomFont';
|
||||
src: url('font.woff2') format('woff2');
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* 2. Match fallback metrics to minimize shift */
|
||||
@font-face {
|
||||
font-family: 'CustomFont-Fallback';
|
||||
src: local('Arial');
|
||||
size-adjust: 105%; /* Scale to match x-height */
|
||||
ascent-override: 90%; /* Match ascender height */
|
||||
descent-override: 20%; /* Match descender depth */
|
||||
line-gap-override: 10%; /* Match line spacing */
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'CustomFont', 'CustomFont-Fallback', sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
Tools like [Fontaine](https://github.com/unjs/fontaine) calculate these overrides automatically.
|
||||
|
||||
## Modern Web Typography
|
||||
|
||||
### Fluid Type
|
||||
|
||||
Fluid typography via `clamp(min, preferred, max)` scales text smoothly with the viewport. The middle value (e.g., `5vw + 1rem`) controls scaling rate—higher vw = faster scaling. Add a rem offset so it doesn't collapse to 0 on small screens.
|
||||
|
||||
**Use fluid type for**: Headings and display text on marketing/content pages where text dominates the layout and needs to breathe across viewport sizes.
|
||||
|
||||
**Use fixed `rem` scales for**: App UIs, dashboards, and data-dense interfaces. No major app design system (Material, Polaris, Primer, Carbon) uses fluid type in product UI — fixed scales with optional breakpoint adjustments give the spatial predictability that container-based layouts need. Body text should also be fixed even on marketing pages, since the size difference across viewports is too small to warrant it.
|
||||
|
||||
### OpenType Features
|
||||
|
||||
Most developers don't know these exist. Use them for polish:
|
||||
|
||||
```css
|
||||
/* Tabular numbers for data alignment */
|
||||
.data-table { font-variant-numeric: tabular-nums; }
|
||||
|
||||
/* Proper fractions */
|
||||
.recipe-amount { font-variant-numeric: diagonal-fractions; }
|
||||
|
||||
/* Small caps for abbreviations */
|
||||
abbr { font-variant-caps: all-small-caps; }
|
||||
|
||||
/* Disable ligatures in code */
|
||||
code { font-variant-ligatures: none; }
|
||||
|
||||
/* Enable kerning (usually on by default, but be explicit) */
|
||||
body { font-kerning: normal; }
|
||||
```
|
||||
|
||||
Check what features your font supports at [Wakamai Fondue](https://wakamaifondue.com/).
|
||||
|
||||
## Typography System Architecture
|
||||
|
||||
Name tokens semantically (`--text-body`, `--text-heading`), not by value (`--font-size-16`). Include font stacks, size scale, weights, line-heights, and letter-spacing in your token system.
|
||||
|
||||
## Accessibility Considerations
|
||||
|
||||
Beyond contrast ratios (which are well-documented), consider:
|
||||
|
||||
- **Never disable zoom**: `user-scalable=no` breaks accessibility. If your layout breaks at 200% zoom, fix the layout.
|
||||
- **Use rem/em for font sizes**: This respects user browser settings. Never `px` for body text.
|
||||
- **Minimum 16px body text**: Smaller than this strains eyes and fails WCAG on mobile.
|
||||
- **Adequate touch targets**: Text links need padding or line-height that creates 44px+ tap targets.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: More than 2-3 font families per project. Skipping fallback font definitions. Ignoring font loading performance (FOUT/FOIT). Using decorative fonts for body text.
|
||||
@@ -1,107 +0,0 @@
|
||||
# UX Writing
|
||||
|
||||
## The Button Label Problem
|
||||
|
||||
**Never use "OK", "Submit", or "Yes/No".** These are lazy and ambiguous. Use specific verb + object patterns:
|
||||
|
||||
| Bad | Good | Why |
|
||||
|-----|------|-----|
|
||||
| OK | Save changes | Says what will happen |
|
||||
| Submit | Create account | Outcome-focused |
|
||||
| Yes | Delete message | Confirms the action |
|
||||
| Cancel | Keep editing | Clarifies what "cancel" means |
|
||||
| Click here | Download PDF | Describes the destination |
|
||||
|
||||
**For destructive actions**, name the destruction:
|
||||
- "Delete" not "Remove" (delete is permanent, remove implies recoverable)
|
||||
- "Delete 5 items" not "Delete selected" (show the count)
|
||||
|
||||
## Error Messages: The Formula
|
||||
|
||||
Every error message should answer: (1) What happened? (2) Why? (3) How to fix it? Example: "Email address isn't valid. Please include an @ symbol." not "Invalid input".
|
||||
|
||||
### Error Message Templates
|
||||
|
||||
| Situation | Template |
|
||||
|-----------|----------|
|
||||
| **Format error** | "[Field] needs to be [format]. Example: [example]" |
|
||||
| **Missing required** | "Please enter [what's missing]" |
|
||||
| **Permission denied** | "You don't have access to [thing]. [What to do instead]" |
|
||||
| **Network error** | "We couldn't reach [thing]. Check your connection and [action]." |
|
||||
| **Server error** | "Something went wrong on our end. We're looking into it. [Alternative action]" |
|
||||
|
||||
### Don't Blame the User
|
||||
|
||||
Reframe errors: "Please enter a date in MM/DD/YYYY format" not "You entered an invalid date".
|
||||
|
||||
## Empty States Are Opportunities
|
||||
|
||||
Empty states are onboarding moments: (1) Acknowledge briefly, (2) Explain the value of filling it, (3) Provide a clear action. "No projects yet. Create your first one to get started." not just "No items".
|
||||
|
||||
## Voice vs Tone
|
||||
|
||||
**Voice** is your brand's personality—consistent everywhere.
|
||||
**Tone** adapts to the moment.
|
||||
|
||||
| Moment | Tone Shift |
|
||||
|--------|------------|
|
||||
| Success | Celebratory, brief: "Done! Your changes are live." |
|
||||
| Error | Empathetic, helpful: "That didn't work. Here's what to try..." |
|
||||
| Loading | Reassuring: "Saving your work..." |
|
||||
| Destructive confirm | Serious, clear: "Delete this project? This can't be undone." |
|
||||
|
||||
**Never use humor for errors.** Users are already frustrated. Be helpful, not cute.
|
||||
|
||||
## Writing for Accessibility
|
||||
|
||||
**Link text** must have standalone meaning—"View pricing plans" not "Click here". **Alt text** describes information, not the image—"Revenue increased 40% in Q4" not "Chart". Use `alt=""` for decorative images. **Icon buttons** need `aria-label` for screen reader context.
|
||||
|
||||
## Writing for Translation
|
||||
|
||||
### Plan for Expansion
|
||||
|
||||
German text is ~30% longer than English. Allocate space:
|
||||
|
||||
| Language | Expansion |
|
||||
|----------|-----------|
|
||||
| German | +30% |
|
||||
| French | +20% |
|
||||
| Finnish | +30-40% |
|
||||
| Chinese | -30% (fewer chars, but same width) |
|
||||
|
||||
### Translation-Friendly Patterns
|
||||
|
||||
Keep numbers separate ("New messages: 3" not "You have 3 new messages"). Use full sentences as single strings (word order varies by language). Avoid abbreviations ("5 minutes ago" not "5 mins ago"). Give translators context about where strings appear.
|
||||
|
||||
## Consistency: The Terminology Problem
|
||||
|
||||
Pick one term and stick with it:
|
||||
|
||||
| Inconsistent | Consistent |
|
||||
|--------------|------------|
|
||||
| Delete / Remove / Trash | Delete |
|
||||
| Settings / Preferences / Options | Settings |
|
||||
| Sign in / Log in / Enter | Sign in |
|
||||
| Create / Add / New | Create |
|
||||
|
||||
Build a terminology glossary and enforce it. Variety creates confusion.
|
||||
|
||||
## Avoid Redundant Copy
|
||||
|
||||
If the heading explains it, the intro is redundant. If the button is clear, don't explain it again. Say it once, say it well.
|
||||
|
||||
## Loading States
|
||||
|
||||
Be specific: "Saving your draft..." not "Loading...". For long waits, set expectations ("This usually takes 30 seconds") or show progress.
|
||||
|
||||
## Confirmation Dialogs: Use Sparingly
|
||||
|
||||
Most confirmation dialogs are design failures—consider undo instead. When you must confirm: name the action, explain consequences, use specific button labels ("Delete project" / "Keep project", not "Yes" / "No").
|
||||
|
||||
## Form Instructions
|
||||
|
||||
Show format with placeholders, not instructions. For non-obvious fields, explain why you're asking.
|
||||
|
||||
---
|
||||
|
||||
**Avoid**: Jargon without explanation. Blaming users ("You made an error" → "This field is required"). Vague errors ("Something went wrong"). Varying terminology for variety. Humor for errors.
|
||||
@@ -1,127 +0,0 @@
|
||||
# MCP server setup
|
||||
|
||||
## 1. Figma remote MCP
|
||||
|
||||
Used for: Reading FA 1.0 designs, extracting component context, getting design
|
||||
properties for reference when building components.
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
# Add the Figma remote MCP to Claude Code
|
||||
claude mcp add --transport http figma-remote-mcp https://mcp.figma.com/mcp
|
||||
|
||||
# Restart Claude Code, then authenticate:
|
||||
# Run /mcp → select figma-remote-mcp → Authenticate
|
||||
# This opens a browser for Figma OAuth
|
||||
```
|
||||
|
||||
### Verify connection
|
||||
|
||||
```bash
|
||||
# In Claude Code:
|
||||
/mcp
|
||||
# Should show: figma-remote-mcp (connected)
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The Figma MCP is link-based. To use it:
|
||||
1. Open your FA 1.0 Figma file
|
||||
2. Select a frame or component
|
||||
3. Copy the link (right-click → Copy link to selection, or copy from browser URL bar)
|
||||
4. Paste the link in your Claude Code prompt
|
||||
|
||||
Example prompts:
|
||||
- "Get the design context for this button: [figma-link]"
|
||||
- "What colours and spacing does this card use? [figma-link]"
|
||||
- "Extract the component structure of this form: [figma-link]"
|
||||
|
||||
### Rate limits
|
||||
|
||||
With a Figma Professional plan, you get per-minute rate limits (Tier 1 REST API
|
||||
equivalent). This is sufficient for component-by-component extraction.
|
||||
|
||||
---
|
||||
|
||||
## 2. Storybook MCP
|
||||
|
||||
Used for: Querying the component library during composition — finding available
|
||||
components, checking props, getting usage examples.
|
||||
|
||||
### Prerequisites
|
||||
|
||||
Storybook must be running locally before the MCP can connect.
|
||||
|
||||
```bash
|
||||
# Start Storybook
|
||||
npm run storybook
|
||||
# This runs on http://localhost:6006 by default
|
||||
```
|
||||
|
||||
### Setup — Standalone Storybook MCP
|
||||
|
||||
```bash
|
||||
# Add the Storybook MCP server to Claude Code
|
||||
claude mcp add storybook --transport http http://localhost:6006/mcp
|
||||
```
|
||||
|
||||
Or add to `.mcp.json` in the project root:
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"storybook": {
|
||||
"url": "http://localhost:6006/mcp",
|
||||
"type": "http"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Note:** Storybook must be running before the MCP server can connect.
|
||||
|
||||
### Verify connection
|
||||
|
||||
```bash
|
||||
# In Claude Code (with Storybook running):
|
||||
/mcp
|
||||
# Should show: storybook (connected)
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
The Storybook MCP exposes tools for querying your component library:
|
||||
- List all available components
|
||||
- Get component props and their types
|
||||
- Find components by keyword
|
||||
- Get usage examples from stories
|
||||
|
||||
This is primarily used by the layout-composer agent (for later phases) to
|
||||
discover and correctly use components when building page layouts.
|
||||
|
||||
---
|
||||
|
||||
## 3. Penpot MCP (for later — layout phase)
|
||||
|
||||
Setup instructions will be added when you reach the layout composition phase.
|
||||
The Penpot MCP requires either the official Penpot MCP plugin running locally,
|
||||
or the Docker-based server alongside a self-hosted Penpot instance.
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Figma MCP not connecting
|
||||
- Restart Claude Code completely (MCP connections initialise at startup)
|
||||
- Re-authenticate: `/mcp` → select figma-remote-mcp → Authenticate
|
||||
- Check you're on a Figma Professional plan (Starter has 6 calls/month)
|
||||
|
||||
### Storybook MCP not connecting
|
||||
- Ensure Storybook is running (`npm run storybook`)
|
||||
- Check the port: default is 6006, but it may use 6007+ if 6006 is occupied
|
||||
- Restart Claude Code after starting Storybook
|
||||
|
||||
### General MCP issues
|
||||
- Run `/mcp` to see status of all servers
|
||||
- Check for error messages in the MCP status output
|
||||
- Try removing and re-adding: `claude mcp remove <name>` then re-add
|
||||
@@ -1,137 +0,0 @@
|
||||
# Retroactive Review Plan
|
||||
|
||||
Components built before the workflow upgrade (2026-03-27) haven't been through
|
||||
the full quality gate lifecycle. This plan reviews them using a lighter process
|
||||
focused on catching real issues, not re-polishing what already works.
|
||||
|
||||
## Approach
|
||||
|
||||
Use the condensed review process from component-lifecycle.md:
|
||||
1. `/normalize {tier}` — Scan the tier for cross-component consistency
|
||||
2. `/audit {component}` — Score each component (only those scoring < 16/20 need fixes)
|
||||
3. Fix P0/P1 issues only
|
||||
4. `/preflight` → commit
|
||||
|
||||
## Priority Order
|
||||
|
||||
Review bottom-up: atoms first (they're the foundation everything builds on),
|
||||
then molecules, then organisms. Within each tier, prioritise by usage —
|
||||
components used by many others matter more than standalone ones.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: Atoms (foundation layer — highest priority)
|
||||
|
||||
### Step 1.1 — Normalize all atoms
|
||||
Run `/normalize atoms` once to get a cross-component consistency report.
|
||||
Expected findings: token access patterns (D031), transition timing, focus
|
||||
styles. Fix all issues in a single batch commit.
|
||||
|
||||
### Step 1.2 — Audit each atom
|
||||
Run `/audit` on each atom. Components already audited in previous sessions have
|
||||
scores on record. Focus on those that scored < 16/20 or were never audited.
|
||||
|
||||
| Atom | Last Audit Score | Priority |
|
||||
|------|-----------------|----------|
|
||||
| Button | **20/20** (2026-03-27) | ~~High~~ Done |
|
||||
| Typography | — | Medium (display-only) |
|
||||
| Input | **20/20** (2026-03-27) | ~~High~~ Done |
|
||||
| Card | **20/20** (2026-03-27, after P1 fixes) | ~~High~~ Done |
|
||||
| Badge | — | Medium (fixed in D031) |
|
||||
| Chip | — | Low (minimal wrapper) |
|
||||
| Switch | — | Low (minimal wrapper) |
|
||||
| Radio | — | Low (minimal wrapper) |
|
||||
| IconButton | — | Low (minimal wrapper) |
|
||||
| Divider | — | Low (minimal wrapper) |
|
||||
| Link | — | Low (minimal wrapper) |
|
||||
|
||||
**Estimated effort:** 1 session for normalize + audit of high/medium priority atoms.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Molecules (composition layer)
|
||||
|
||||
### Step 2.1 — Normalize all molecules
|
||||
Run `/normalize molecules` for cross-component consistency.
|
||||
|
||||
### Step 2.2 — Audit + critique priority molecules
|
||||
Run `/audit` and `/critique` on molecules with real layout complexity.
|
||||
|
||||
| Molecule | Last Scores | Priority |
|
||||
|----------|------------|----------|
|
||||
| ProviderCard | Critique 33/40 (v2 iteration) | Medium (user-approved) |
|
||||
| VenueCard | Critique 33/40 | Medium (user-approved) |
|
||||
| SearchBar | Critique 35/40 | Low (high scores already) |
|
||||
| ServiceOption | **Audit 13/20** (2026-03-30, P1 fixed) | ~~Medium~~ Done |
|
||||
| AddOnOption | **Audit 14/20** (2026-03-30, P1 fixed) | ~~Medium~~ Done |
|
||||
| StepIndicator | — | Low (display-only) |
|
||||
| LineItem | Audit 19/20 | Low (excellent score) |
|
||||
| ProviderCardCompact | **Audit 15/20** (2026-03-30, P2 fixed) | ~~Medium~~ Done |
|
||||
|
||||
**Estimated effort:** 1 session for normalize + audit of medium priority molecules.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: Organisms (page-level compositions)
|
||||
|
||||
### Step 3.1 — Normalize all organisms
|
||||
Run `/normalize organisms` for cross-component consistency.
|
||||
|
||||
### Step 3.2 — Full review of critical organisms
|
||||
Organisms are the most complex and user-facing. Run `/audit` + `/critique` +
|
||||
`/harden` on each.
|
||||
|
||||
| Organism | Last Scores | Priority |
|
||||
|----------|------------|----------|
|
||||
| Navigation | — | High (site-wide, visible on every page) |
|
||||
| Footer | Critique 38/40 | Low (excellent score) |
|
||||
| ServiceSelector | — | High (arrangement flow core) |
|
||||
| PackageDetail | Audit 19/20 | Low (excellent score) |
|
||||
| FuneralFinder V1 | Audit 14/20, Critique 29/40 | Medium (pending production decision) |
|
||||
| FuneralFinder V2 | Audit 18/20, Critique 33/40 | Medium (pending production decision) |
|
||||
| FuneralFinder V3 | Audit 18/20, Critique 33/40 | Medium (pending production decision) |
|
||||
|
||||
**Note on FuneralFinder:** All three versions exist. A production decision (v1 vs v2 vs v3)
|
||||
is still pending. Only review the chosen version in depth. The others can be archived or
|
||||
retained as alternatives.
|
||||
|
||||
**Estimated effort:** 1 session for normalize + audit/critique of Navigation + ServiceSelector.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: Cross-cutting concerns
|
||||
|
||||
After individual components are clean:
|
||||
1. ~~**Form error colour normalisation (D034):**~~ Done (2026-03-28). Copper error styling in MuiOutlinedInput, MuiFormHelperText, MuiFormLabel, ToggleButtonGroup.
|
||||
2. ~~Run `/adapt` on all organisms + ProviderCard/VenueCard (responsive check)~~ Done (2026-03-31d). Navigation + Footer touch targets fixed (P0/P1). ProviderCard/VenueCard: no action needed (card is the touch target).
|
||||
3. Run `/typeset` across a representative sample of each tier
|
||||
4. Run `/preflight` to verify the full codebase
|
||||
5. Commit all fixes
|
||||
|
||||
**Estimated effort:** 0.5 session (remaining: typeset + preflight only).
|
||||
|
||||
---
|
||||
|
||||
## Total estimated effort: ~3.5 sessions
|
||||
|
||||
| Phase | Focus | Effort |
|
||||
|-------|-------|--------|
|
||||
| 1 | Atoms normalize + audit | 1 session |
|
||||
| 2 | Molecules normalize + audit | 1 session |
|
||||
| 3 | Organisms normalize + audit/critique/harden | 1 session |
|
||||
| 4 | Cross-cutting (adapt, typeset, preflight) | 0.5 session |
|
||||
|
||||
This can be interleaved with new component work — e.g., review atoms in the
|
||||
morning, build a new molecule in the afternoon. The review findings often
|
||||
improve the patterns used in new components.
|
||||
|
||||
---
|
||||
|
||||
## Decisions (confirmed 2026-03-27)
|
||||
|
||||
1. **FuneralFinder version** — V3 is production. V1/V2 archived (viewable in
|
||||
Storybook under Archive/). Only V3 gets the full review.
|
||||
2. **Depth** — P0/P1 only. Don't touch P2/P3 — not worth the risk on approved components.
|
||||
3. **Approach** — Interleaved. Start each session with a review pass (~30-60 min),
|
||||
then shift to new component work. This is automated — the agent runs it
|
||||
proactively at session start without being asked.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,145 +0,0 @@
|
||||
---
|
||||
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`
|
||||
@@ -1,39 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,179 +0,0 @@
|
||||
---
|
||||
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 MB`, `⌘ 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.
|
||||
@@ -69,7 +69,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
const copyrightText = copyright || `\u00A9 ${year} Funeral Arranger. All rights reserved.`;
|
||||
|
||||
const overlineSx = {
|
||||
color: 'var(--fa-color-brand-400)',
|
||||
color: 'text.secondary',
|
||||
textTransform: 'uppercase' as const,
|
||||
letterSpacing: '0.08em',
|
||||
display: 'block',
|
||||
@@ -77,8 +77,8 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
};
|
||||
|
||||
const contactLinkSx = {
|
||||
color: 'var(--fa-color-white)',
|
||||
'&:hover': { color: 'var(--fa-color-brand-300)' },
|
||||
color: 'text.primary',
|
||||
'&:hover': { color: 'var(--fa-color-brand-600)' },
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -87,8 +87,8 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
component="footer"
|
||||
sx={[
|
||||
{
|
||||
bgcolor: 'var(--fa-color-brand-950)',
|
||||
color: 'var(--fa-color-white)',
|
||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||
color: 'text.primary',
|
||||
pt: { xs: 5, md: 8 },
|
||||
pb: 0,
|
||||
},
|
||||
@@ -104,7 +104,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
{tagline && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ color: 'var(--fa-color-brand-300)', maxWidth: { xs: '100%', md: 280 } }}
|
||||
sx={{ color: 'text.secondary', maxWidth: { xs: '100%', md: 280 } }}
|
||||
>
|
||||
{tagline}
|
||||
</Typography>
|
||||
@@ -168,7 +168,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
<Typography
|
||||
variant="label"
|
||||
sx={{
|
||||
color: 'var(--fa-color-brand-300)',
|
||||
color: 'text.secondary',
|
||||
mb: 2,
|
||||
display: 'block',
|
||||
}}
|
||||
@@ -192,13 +192,13 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
sx={{
|
||||
color: 'var(--fa-color-brand-200)',
|
||||
color: 'text.primary',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 44,
|
||||
'&:hover': { color: 'var(--fa-color-white)' },
|
||||
'&:hover': { color: 'var(--fa-color-brand-600)' },
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
@@ -211,7 +211,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
</Grid>
|
||||
|
||||
{/* Bottom bar */}
|
||||
<Divider sx={{ borderColor: 'var(--fa-color-brand-900)', mt: { xs: 5, md: 8 } }} />
|
||||
<Divider sx={{ mt: { xs: 5, md: 8 } }} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
@@ -223,7 +223,7 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
py: 3,
|
||||
}}
|
||||
>
|
||||
<Typography variant="captionSm" sx={{ color: 'var(--fa-color-brand-400)' }}>
|
||||
<Typography variant="captionSm" sx={{ color: 'text.secondary' }}>
|
||||
{copyrightText}
|
||||
</Typography>
|
||||
|
||||
@@ -242,13 +242,13 @@ export const Footer = React.forwardRef<HTMLDivElement, FooterProps>(
|
||||
href={link.href}
|
||||
onClick={link.onClick}
|
||||
sx={{
|
||||
color: 'var(--fa-color-brand-400)',
|
||||
color: 'text.secondary',
|
||||
fontSize: '0.75rem',
|
||||
fontWeight: 500,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
minHeight: 44,
|
||||
'&:hover': { color: 'var(--fa-color-white)' },
|
||||
'&:hover': { color: 'var(--fa-color-brand-600)' },
|
||||
}}
|
||||
>
|
||||
{link.label}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Divider } from '../../atoms/Divider';
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type Status = 'immediate' | 'preplanning';
|
||||
type Timeframe = 'soon' | 'future';
|
||||
type FuneralType =
|
||||
| 'cremation-funeral'
|
||||
| 'cremation-only'
|
||||
@@ -26,6 +27,8 @@ type FuneralType =
|
||||
export interface FuneralFinderV3SearchParams {
|
||||
/** User's current situation */
|
||||
status: Status;
|
||||
/** Pre-planning timeframe — only present when status is 'preplanning' */
|
||||
timeframe?: Timeframe;
|
||||
/** Type of funeral selected (defaults to show-all if not chosen) */
|
||||
funeralType: FuneralType;
|
||||
/** Suburb or postcode */
|
||||
@@ -61,6 +64,11 @@ const STATUS_OPTIONS: { key: Status; title: string; description: string }[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const TIMEFRAME_OPTIONS: { key: Timeframe; label: string }[] = [
|
||||
{ key: 'soon', label: 'In the coming weeks or months' },
|
||||
{ key: 'future', label: 'No set timeframe, I\u2019m planning ahead' },
|
||||
];
|
||||
|
||||
const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
|
||||
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
|
||||
{
|
||||
@@ -150,12 +158,14 @@ const StatusCard = React.forwardRef<
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
display: 'block',
|
||||
mb: 0.75,
|
||||
fontSize: { xs: '0.875rem', sm: '1rem' },
|
||||
mb: description ? 0.75 : 0,
|
||||
color: selected ? 'var(--fa-color-text-brand, #B0610F)' : 'text.primary',
|
||||
}}
|
||||
>
|
||||
{title}
|
||||
</Typography>
|
||||
{description && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
component="span"
|
||||
@@ -167,6 +177,7 @@ const StatusCard = React.forwardRef<
|
||||
>
|
||||
{description}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
));
|
||||
StatusCard.displayName = 'StatusCard';
|
||||
@@ -195,9 +206,9 @@ const fieldBaseSx = {
|
||||
};
|
||||
|
||||
const fieldInputStyles = {
|
||||
py: '14px',
|
||||
py: '12px',
|
||||
px: 2,
|
||||
fontSize: '1rem',
|
||||
fontSize: '0.9375rem',
|
||||
fontFamily: 'var(--fa-font-family-body)',
|
||||
};
|
||||
|
||||
@@ -244,7 +255,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
onSearch,
|
||||
loading = false,
|
||||
heading = 'Find funeral directors near you',
|
||||
subheading = 'Tell us what you need and we\u2019ll show options in your area.',
|
||||
subheading,
|
||||
sx,
|
||||
} = props;
|
||||
|
||||
@@ -256,10 +267,12 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
|
||||
// ─── State ───────────────────────────────────────────────
|
||||
const [status, setStatus] = React.useState<Status | ''>('immediate');
|
||||
const [timeframe, setTimeframe] = React.useState<Timeframe | ''>('');
|
||||
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||
const [location, setLocation] = React.useState('');
|
||||
const [errors, setErrors] = React.useState<{
|
||||
status?: boolean;
|
||||
timeframe?: boolean;
|
||||
location?: boolean;
|
||||
}>({});
|
||||
|
||||
@@ -290,6 +303,8 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
}
|
||||
}, [location, errors.location]);
|
||||
|
||||
const isPrePlanning = status === 'preplanning';
|
||||
|
||||
// ─── Radiogroup keyboard nav (WAI-ARIA pattern) ──────────
|
||||
const activeStatusIndex = status ? STATUS_OPTIONS.findIndex((o) => o.key === status) : 0;
|
||||
|
||||
@@ -305,10 +320,18 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
: Math.max(current - 1, 0);
|
||||
if (next !== current) {
|
||||
cardRefs.current[next]?.focus();
|
||||
setStatus(STATUS_OPTIONS[next].key);
|
||||
const newStatus = STATUS_OPTIONS[next].key;
|
||||
setStatus(newStatus);
|
||||
if (newStatus !== 'preplanning') setTimeframe('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleStatusClick = (key: Status) => {
|
||||
setStatus(key);
|
||||
if (key !== 'preplanning') setTimeframe('');
|
||||
if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
|
||||
};
|
||||
|
||||
// ─── Handlers ────────────────────────────────────────────
|
||||
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||
setFuneralType(e.target.value as FuneralType);
|
||||
@@ -323,6 +346,14 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (status === 'preplanning' && !timeframe) {
|
||||
setErrors({ timeframe: true });
|
||||
statusSectionRef.current?.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'center',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (location.trim().length < 3) {
|
||||
setErrors({ location: true });
|
||||
locationSectionRef.current?.scrollIntoView({
|
||||
@@ -335,6 +366,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
setErrors({});
|
||||
onSearch?.({
|
||||
status,
|
||||
...(status === 'preplanning' && timeframe ? { timeframe } : {}),
|
||||
funeralType: funeralType || 'show-all',
|
||||
location: location.trim(),
|
||||
});
|
||||
@@ -355,7 +387,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
py: { xs: 4, sm: 5 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
gap: { xs: 3, sm: 4 },
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
@@ -367,15 +399,18 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
component="h2"
|
||||
sx={{
|
||||
fontFamily: 'var(--fa-font-family-display)',
|
||||
fontWeight: 400,
|
||||
mb: 1,
|
||||
fontWeight: 600,
|
||||
fontSize: { xs: '1.25rem', sm: '1.5rem' },
|
||||
mb: subheading ? 1 : 0,
|
||||
}}
|
||||
>
|
||||
{heading}
|
||||
</Typography>
|
||||
{subheading && (
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{subheading}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
<Divider />
|
||||
@@ -402,7 +437,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
title={opt.title}
|
||||
description={opt.description}
|
||||
selected={status === opt.key}
|
||||
onClick={() => setStatus(opt.key)}
|
||||
onClick={() => handleStatusClick(opt.key)}
|
||||
tabIndex={i === activeStatusIndex ? 0 : -1}
|
||||
onKeyDown={handleStatusKeyDown}
|
||||
/>
|
||||
@@ -423,6 +458,63 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ── Timeframe follow-up (pre-planning only) ────────── */}
|
||||
{isPrePlanning && (
|
||||
<Box
|
||||
sx={{
|
||||
mt: 3,
|
||||
overflow: 'hidden',
|
||||
animation: 'fadeSlideIn 200ms ease-out',
|
||||
'@keyframes fadeSlideIn': {
|
||||
'0%': { opacity: 0, transform: 'translateY(-8px)' },
|
||||
'100%': { opacity: 1, transform: 'translateY(0)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<SectionLabel id={`${id}-timeframe`}>How Soon Might You Need This?</SectionLabel>
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-labelledby={`${id}-timeframe`}
|
||||
sx={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', sm: '1fr 1fr' },
|
||||
gap: 2,
|
||||
mt: 2,
|
||||
'& button': { fontSize: '0.8125rem' },
|
||||
'& button .MuiTypography-root': { fontSize: '0.8125rem' },
|
||||
}}
|
||||
>
|
||||
{TIMEFRAME_OPTIONS.map((opt) => (
|
||||
<StatusCard
|
||||
key={opt.key}
|
||||
title={opt.label}
|
||||
description=""
|
||||
selected={timeframe === opt.key}
|
||||
onClick={() => {
|
||||
setTimeframe(opt.key);
|
||||
if (errors.timeframe) setErrors((prev) => ({ ...prev, timeframe: false }));
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
<Box aria-live="polite" sx={{ textAlign: 'center' }}>
|
||||
{errors.timeframe && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
role="alert"
|
||||
sx={{
|
||||
color: 'var(--fa-color-text-brand, #B0610F)',
|
||||
display: 'block',
|
||||
mt: 1,
|
||||
}}
|
||||
>
|
||||
Please select a timeframe
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ── Funeral Type ────────────────────────────────────── */}
|
||||
@@ -527,7 +619,7 @@ export const FuneralFinderV3 = React.forwardRef<HTMLDivElement, FuneralFinderV3P
|
||||
onClick={handleSubmit}
|
||||
sx={{ minHeight: 52 }}
|
||||
>
|
||||
Find Funeral Directors
|
||||
Search Local Providers
|
||||
</Button>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import { FuneralFinderV4 } from './FuneralFinderV4';
|
||||
|
||||
const meta: Meta<typeof FuneralFinderV4> = {
|
||||
title: 'Archive/FuneralFinder V4',
|
||||
component: FuneralFinderV4,
|
||||
parameters: {
|
||||
layout: 'padded',
|
||||
},
|
||||
args: {
|
||||
onSearch: (params) => {
|
||||
console.log('Search params:', params);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof FuneralFinderV4>;
|
||||
|
||||
/** Default empty state — 3 steps + location ready for input */
|
||||
export const Default: Story = {};
|
||||
|
||||
/** Loading state — CTA shows spinner */
|
||||
export const Loading: Story = {
|
||||
args: { loading: true },
|
||||
};
|
||||
|
||||
/** Placed inside a dark hero section to preview in context */
|
||||
export const InsideHero: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box>
|
||||
<Box
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-sage-800, #4c5459)',
|
||||
color: '#fff',
|
||||
py: 8,
|
||||
px: 4,
|
||||
textAlign: 'center',
|
||||
}}
|
||||
>
|
||||
<Box sx={{ fontSize: '2.5rem', fontWeight: 700, mb: 1 }}>Funeral Arranger</Box>
|
||||
<Box sx={{ opacity: 0.8, mb: 4 }}>Find trusted funeral directors near you</Box>
|
||||
<Box sx={{ maxWidth: 480, mx: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
};
|
||||
|
||||
/** Constrained width — typical sidebar or narrow column */
|
||||
export const Narrow: Story = {
|
||||
decorators: [
|
||||
(Story) => (
|
||||
<Box sx={{ maxWidth: 380, mx: 'auto' }}>
|
||||
<Story />
|
||||
</Box>
|
||||
),
|
||||
],
|
||||
};
|
||||
493
src/components/organisms/FuneralFinder/FuneralFinderV4.tsx
Normal file
493
src/components/organisms/FuneralFinder/FuneralFinderV4.tsx
Normal file
@@ -0,0 +1,493 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Select, { type SelectChangeEvent } from '@mui/material/Select';
|
||||
import MenuItem from '@mui/material/MenuItem';
|
||||
import CheckIcon from '@mui/icons-material/Check';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Input } from '../../atoms/Input';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
type LookingTo = 'arrange-now' | 'expected' | 'future';
|
||||
type PlanningFor = 'myself' | 'someone-else';
|
||||
type FuneralType =
|
||||
| 'cremation-funeral'
|
||||
| 'cremation-only'
|
||||
| 'burial-funeral'
|
||||
| 'graveside-only'
|
||||
| 'water-cremation';
|
||||
|
||||
/** Search parameters returned on form submission */
|
||||
export interface FuneralFinderV4SearchParams {
|
||||
/** User's situation — immediate, expected, or future need */
|
||||
lookingTo: LookingTo;
|
||||
/** Who the funeral is for */
|
||||
planningFor: PlanningFor;
|
||||
/** Type of funeral selected */
|
||||
funeralType: FuneralType;
|
||||
/** Suburb or postcode */
|
||||
location: string;
|
||||
}
|
||||
|
||||
/** Props for the FuneralFinder v4 organism */
|
||||
export interface FuneralFinderV4Props {
|
||||
/** Called when the user submits with valid data */
|
||||
onSearch?: (params: FuneralFinderV4SearchParams) => void;
|
||||
/** Shows loading state on the CTA */
|
||||
loading?: boolean;
|
||||
/** MUI sx override for the root container */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Options ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const LOOKING_TO_OPTIONS: { value: LookingTo; label: string }[] = [
|
||||
{ value: 'arrange-now', label: 'Arrange a funeral for someone who has passed' },
|
||||
{ value: 'expected', label: 'Plan ahead for someone who is unwell' },
|
||||
{ value: 'future', label: "Plan for a future need that isn't expected soon" },
|
||||
];
|
||||
|
||||
const PLANNING_FOR_OPTIONS: { value: PlanningFor; label: string }[] = [
|
||||
{ value: 'someone-else', label: 'Someone else' },
|
||||
{ value: 'myself', label: 'Myself' },
|
||||
];
|
||||
|
||||
const FUNERAL_TYPE_OPTIONS: { value: FuneralType; label: string }[] = [
|
||||
{ value: 'cremation-funeral', label: 'Cremation with funeral' },
|
||||
{ value: 'cremation-only', label: 'Cremation only (no funeral, no attendance)' },
|
||||
{ value: 'burial-funeral', label: 'Burial with funeral' },
|
||||
{ value: 'graveside-only', label: 'Graveside burial only' },
|
||||
{ value: 'water-cremation', label: 'Water cremation (QLD only)' },
|
||||
];
|
||||
|
||||
// ─── Step indicator ─────────────────────────────────────────────────────────
|
||||
|
||||
const INDICATOR_SIZE = 48;
|
||||
const ICON_SIZE = 20;
|
||||
|
||||
type StepState = 'inactive' | 'active' | 'completed';
|
||||
|
||||
function StepIndicator({ step, state }: { step: number; state: StepState }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
width: INDICATOR_SIZE,
|
||||
height: INDICATOR_SIZE,
|
||||
borderRadius: '50%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flexShrink: 0,
|
||||
transition: 'all 250ms cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
...(state === 'completed' && {
|
||||
bgcolor: 'var(--fa-color-brand-500)',
|
||||
color: 'common.white',
|
||||
boxShadow: '0 0 0 3px var(--fa-color-brand-100, #F5EDE4)',
|
||||
}),
|
||||
...(state === 'active' && {
|
||||
bgcolor: 'var(--fa-color-brand-500)',
|
||||
color: 'common.white',
|
||||
boxShadow: '0 0 0 3px var(--fa-color-brand-100, #F5EDE4)',
|
||||
}),
|
||||
...(state === 'inactive' && {
|
||||
bgcolor: 'transparent',
|
||||
border: '2px solid var(--fa-color-neutral-300, #C4C4C4)',
|
||||
color: 'var(--fa-color-neutral-400, #9E9E9E)',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
{state === 'completed' ? (
|
||||
<CheckIcon
|
||||
sx={{
|
||||
fontSize: ICON_SIZE,
|
||||
animation: 'fadeScaleIn 250ms cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
'@keyframes fadeScaleIn': {
|
||||
'0%': { opacity: 0, transform: 'scale(0.5)' },
|
||||
'100%': { opacity: 1, transform: 'scale(1)' },
|
||||
},
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
component="span"
|
||||
sx={{
|
||||
fontWeight: 700,
|
||||
fontSize: '1.125rem',
|
||||
lineHeight: 1,
|
||||
color: 'inherit',
|
||||
}}
|
||||
>
|
||||
{step}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
/** Inline error message shown below a field */
|
||||
function FieldError({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
role="alert"
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
color: 'var(--fa-color-brand-600, #B0610F)',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Typography>
|
||||
);
|
||||
}
|
||||
|
||||
// ─── Shared select styles ───────────────────────────────────────────────────
|
||||
|
||||
const selectSx: SxProps<Theme> = {
|
||||
width: '100%',
|
||||
bgcolor: 'var(--fa-color-surface-default, #fff)',
|
||||
'.MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-neutral-200)',
|
||||
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||
},
|
||||
'&:hover:not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-400)',
|
||||
},
|
||||
'&.Mui-focused': { boxShadow: 'none' },
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-400)',
|
||||
borderWidth: 1,
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.6,
|
||||
'.MuiOutlinedInput-notchedOutline': { borderStyle: 'dashed' },
|
||||
},
|
||||
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-600, #B0610F)',
|
||||
},
|
||||
'.MuiSelect-select': {
|
||||
py: '14px',
|
||||
px: 2,
|
||||
fontSize: '0.875rem',
|
||||
minHeight: 'unset !important',
|
||||
},
|
||||
'.MuiSelect-icon': { color: 'var(--fa-color-neutral-400)' },
|
||||
};
|
||||
|
||||
const selectMenuProps = {
|
||||
PaperProps: {
|
||||
sx: {
|
||||
mt: 0.5,
|
||||
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||
boxShadow: 'var(--fa-shadow-md)',
|
||||
minWidth: 280,
|
||||
'& .MuiMenuItem-root': {
|
||||
py: 1.5,
|
||||
px: 2,
|
||||
fontSize: '0.875rem',
|
||||
whiteSpace: 'normal',
|
||||
'&:hover': { bgcolor: 'var(--fa-color-brand-50)' },
|
||||
'&.Mui-selected': {
|
||||
bgcolor: 'var(--fa-color-surface-warm)',
|
||||
fontWeight: 600,
|
||||
'&:hover': { bgcolor: 'var(--fa-color-surface-warm)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Component ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* FuneralFinder V4 — compact search widget.
|
||||
*
|
||||
* Based on V2's field set with a streamlined layout:
|
||||
* - No heading/subheading — designed to sit inside a hero or card
|
||||
* - 3 numbered steps (intent, planning-for, funeral type) with refined indicators
|
||||
* - Location field is always enabled (not a numbered step)
|
||||
* - "Search" CTA
|
||||
*
|
||||
* Conditional logic:
|
||||
* - "Arrange a funeral for someone who has passed" auto-sets step 2
|
||||
* to "Someone else" and disables it.
|
||||
* - "Myself" is only available for pre-planning paths (expected / future).
|
||||
* - Steps 2 and 3 unlock sequentially; location is always available.
|
||||
*/
|
||||
export const FuneralFinderV4 = React.forwardRef<HTMLDivElement, FuneralFinderV4Props>(
|
||||
({ onSearch, loading = false, sx }, ref) => {
|
||||
// ─── State ───────────────────────────────────────────────────
|
||||
const [lookingTo, setLookingTo] = React.useState<LookingTo | ''>('');
|
||||
const [planningFor, setPlanningFor] = React.useState<PlanningFor | ''>('');
|
||||
const [funeralType, setFuneralType] = React.useState<FuneralType | ''>('');
|
||||
const [location, setLocation] = React.useState('');
|
||||
const [submitted, setSubmitted] = React.useState(false);
|
||||
|
||||
// ─── Derived ─────────────────────────────────────────────────
|
||||
const isArrangeNow = lookingTo === 'arrange-now';
|
||||
const step2Disabled = !lookingTo || isArrangeNow;
|
||||
const step3Disabled = !planningFor;
|
||||
|
||||
// Step states for indicators
|
||||
const step1State: StepState = lookingTo ? 'completed' : 'active';
|
||||
const step2State: StepState = planningFor ? 'completed' : lookingTo ? 'active' : 'inactive';
|
||||
const step3State: StepState = funeralType ? 'completed' : planningFor ? 'active' : 'inactive';
|
||||
|
||||
// Errors only show after first submit attempt
|
||||
const errs = submitted
|
||||
? {
|
||||
lookingTo: !lookingTo,
|
||||
planningFor: !planningFor,
|
||||
funeralType: !funeralType,
|
||||
location: location.trim().length < 3,
|
||||
}
|
||||
: { lookingTo: false, planningFor: false, funeralType: false, location: false };
|
||||
|
||||
// ─── Handlers ────────────────────────────────────────────────
|
||||
const handleLookingTo = (e: SelectChangeEvent<string>) => {
|
||||
const val = e.target.value as LookingTo;
|
||||
setLookingTo(val);
|
||||
if (val === 'arrange-now') {
|
||||
setPlanningFor('someone-else');
|
||||
} else {
|
||||
setPlanningFor('');
|
||||
}
|
||||
};
|
||||
|
||||
const handlePlanningFor = (e: SelectChangeEvent<string>) => {
|
||||
setPlanningFor(e.target.value as PlanningFor);
|
||||
};
|
||||
|
||||
const handleFuneralType = (e: SelectChangeEvent<string>) => {
|
||||
setFuneralType(e.target.value as FuneralType);
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
setSubmitted(true);
|
||||
if (!lookingTo || !planningFor || !funeralType || location.trim().length < 3) return;
|
||||
onSearch?.({
|
||||
lookingTo,
|
||||
planningFor,
|
||||
funeralType,
|
||||
location: location.trim(),
|
||||
});
|
||||
};
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────
|
||||
const placeholder = (
|
||||
<span style={{ color: 'var(--fa-color-neutral-400)' }}>Select…</span>
|
||||
);
|
||||
|
||||
const findLabel = (opts: { value: string; label: string }[], val: string) =>
|
||||
opts.find((o) => o.value === val)?.label ?? '';
|
||||
|
||||
// ─── Render ──────────────────────────────────────────────────
|
||||
return (
|
||||
<Box
|
||||
ref={ref}
|
||||
role="search"
|
||||
aria-label="Find funeral providers"
|
||||
sx={[
|
||||
{
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 'var(--fa-card-border-radius-default, 12px)',
|
||||
boxShadow: 'var(--fa-shadow-md)',
|
||||
textAlign: 'left',
|
||||
px: { xs: 3, sm: 5 },
|
||||
py: { xs: 3, sm: 4 },
|
||||
},
|
||||
...(Array.isArray(sx) ? sx : [sx]),
|
||||
]}
|
||||
>
|
||||
{/* ── Steps ───────────────────────────────────────────── */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 3.5 }}>
|
||||
{/* Step 1: I'm looking to */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{ fontWeight: 600, mb: 0.75, color: 'var(--fa-color-brand-700)' }}
|
||||
>
|
||||
I’m looking to…
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<StepIndicator step={1} state={step1State} />
|
||||
<Select
|
||||
value={lookingTo}
|
||||
onChange={handleLookingTo}
|
||||
displayEmpty
|
||||
error={errs.lookingTo}
|
||||
renderValue={(v) => (v ? findLabel(LOOKING_TO_OPTIONS, v) : placeholder)}
|
||||
MenuProps={selectMenuProps}
|
||||
sx={{ ...selectSx, flex: 1 } as SxProps<Theme>}
|
||||
inputProps={{ 'aria-label': "I'm looking to", 'aria-required': true }}
|
||||
>
|
||||
{LOOKING_TO_OPTIONS.map((o) => (
|
||||
<MenuItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
{errs.lookingTo && <FieldError>Please tell us what you need help with</FieldError>}
|
||||
</Box>
|
||||
|
||||
{/* Step 2: I'm planning for */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 0.75,
|
||||
color: lookingTo ? 'var(--fa-color-brand-700)' : 'text.disabled',
|
||||
}}
|
||||
>
|
||||
I’m planning for…
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<StepIndicator step={2} state={step2State} />
|
||||
<Select
|
||||
value={planningFor}
|
||||
onChange={handlePlanningFor}
|
||||
displayEmpty
|
||||
disabled={step2Disabled}
|
||||
error={errs.planningFor}
|
||||
renderValue={(v) => (v ? findLabel(PLANNING_FOR_OPTIONS, v) : placeholder)}
|
||||
MenuProps={selectMenuProps}
|
||||
sx={{ ...selectSx, flex: 1 } as SxProps<Theme>}
|
||||
inputProps={{ 'aria-label': "I'm planning for", 'aria-required': true }}
|
||||
>
|
||||
{PLANNING_FOR_OPTIONS.map((o) => (
|
||||
<MenuItem
|
||||
key={o.value}
|
||||
value={o.value}
|
||||
disabled={isArrangeNow && o.value === 'myself'}
|
||||
>
|
||||
{o.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
{errs.planningFor && <FieldError>Please select who you are planning for</FieldError>}
|
||||
</Box>
|
||||
|
||||
{/* Step 3: Type of funeral */}
|
||||
<Box>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 0.75,
|
||||
color: step3Disabled ? 'text.disabled' : 'var(--fa-color-brand-700)',
|
||||
}}
|
||||
>
|
||||
Type of funeral
|
||||
</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<StepIndicator step={3} state={step3State} />
|
||||
<Select
|
||||
value={funeralType}
|
||||
onChange={handleFuneralType}
|
||||
displayEmpty
|
||||
disabled={step3Disabled}
|
||||
error={errs.funeralType}
|
||||
renderValue={(v) => (v ? findLabel(FUNERAL_TYPE_OPTIONS, v) : placeholder)}
|
||||
MenuProps={selectMenuProps}
|
||||
sx={{ ...selectSx, flex: 1 } as SxProps<Theme>}
|
||||
inputProps={{ 'aria-label': 'Type of funeral', 'aria-required': true }}
|
||||
>
|
||||
{FUNERAL_TYPE_OPTIONS.map((o) => (
|
||||
<MenuItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
</Box>
|
||||
{errs.funeralType && <FieldError>Please select a funeral type</FieldError>}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* ── Location (not a numbered step) ─────────────────── */}
|
||||
<Box sx={{ mt: 3.5 }}>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
fontWeight: 600,
|
||||
mb: 0.75,
|
||||
color: funeralType ? 'var(--fa-color-brand-700)' : 'text.disabled',
|
||||
}}
|
||||
>
|
||||
Looking for providers in
|
||||
</Typography>
|
||||
<Input
|
||||
placeholder="Suburb or postcode"
|
||||
value={location}
|
||||
onChange={(e) => setLocation(e.target.value)}
|
||||
fullWidth
|
||||
disabled={!funeralType}
|
||||
error={errs.location}
|
||||
inputProps={{ 'aria-label': 'Suburb or postcode', 'aria-required': true }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') handleSubmit();
|
||||
}}
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-default, #fff)',
|
||||
'& .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-neutral-200)',
|
||||
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||
},
|
||||
'&:hover:not(.Mui-disabled) .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-400)',
|
||||
},
|
||||
'&.Mui-focused': { boxShadow: 'none' },
|
||||
'&.Mui-focused .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-400)',
|
||||
borderWidth: '1px',
|
||||
},
|
||||
'&.Mui-disabled': {
|
||||
opacity: 0.6,
|
||||
'& .MuiOutlinedInput-notchedOutline': { borderStyle: 'dashed' },
|
||||
},
|
||||
'&.Mui-error .MuiOutlinedInput-notchedOutline': {
|
||||
borderColor: 'var(--fa-color-brand-600, #B0610F)',
|
||||
},
|
||||
'& .MuiOutlinedInput-input': {
|
||||
py: '14px',
|
||||
px: 2,
|
||||
fontSize: '0.875rem',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
{errs.location && <FieldError>Please enter a suburb or postcode</FieldError>}
|
||||
</Box>
|
||||
|
||||
{/* ── CTA ─────────────────────────────────────────────── */}
|
||||
<Divider sx={{ my: 3 }} />
|
||||
<Box>
|
||||
<Button
|
||||
variant="contained"
|
||||
size="large"
|
||||
fullWidth
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
Search
|
||||
</Button>
|
||||
<Typography
|
||||
variant="captionSm"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', display: 'block', mt: 1.5 }}
|
||||
>
|
||||
Free to use · No obligation
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FuneralFinderV4.displayName = 'FuneralFinderV4';
|
||||
export default FuneralFinderV4;
|
||||
@@ -1,5 +1,6 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import InfoOutlinedIcon from '@mui/icons-material/InfoOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
@@ -50,6 +51,12 @@ export interface PackageDetailProps {
|
||||
arrangeDisabled?: boolean;
|
||||
/** Whether the compare button is in loading state */
|
||||
compareLoading?: boolean;
|
||||
/** Custom label for the arrange CTA button (default: "Make Arrangement") */
|
||||
arrangeLabel?: string;
|
||||
/** Disclaimer shown below the price (e.g. for unverified/estimated pricing) */
|
||||
priceDisclaimer?: string;
|
||||
/** When true, replaces the itemised breakdown with an "Itemised Pricing Unavailable" notice */
|
||||
itemizedUnavailable?: boolean;
|
||||
/** MUI sx prop for the root element */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
@@ -116,6 +123,9 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
onCompare,
|
||||
arrangeDisabled = false,
|
||||
compareLoading = false,
|
||||
arrangeLabel = 'Make Arrangement',
|
||||
priceDisclaimer,
|
||||
itemizedUnavailable = false,
|
||||
sx,
|
||||
},
|
||||
ref,
|
||||
@@ -159,6 +169,32 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
${price.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
|
||||
{/* Price disclaimer */}
|
||||
{priceDisclaimer && (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
gap: 1,
|
||||
mt: 1.5,
|
||||
px: 1.5,
|
||||
py: 1,
|
||||
bgcolor: 'var(--fa-color-surface-cool, #F5F7FA)',
|
||||
borderRadius: 'var(--fa-border-radius-sm, 6px)',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
}}
|
||||
>
|
||||
<InfoOutlinedIcon
|
||||
sx={{ fontSize: 16, color: 'text.secondary', mt: '1px', flexShrink: 0 }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ lineHeight: 1.4 }}>
|
||||
{priceDisclaimer}
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* CTA buttons */}
|
||||
<Box
|
||||
sx={{ display: 'flex', flexDirection: { xs: 'column', sm: 'row' }, gap: 1.5, mt: 2.5 }}
|
||||
@@ -170,7 +206,7 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
disabled={arrangeDisabled}
|
||||
onClick={onArrange}
|
||||
>
|
||||
Make Arrangement
|
||||
{arrangeLabel}
|
||||
</Button>
|
||||
{onCompare && (
|
||||
<Button
|
||||
@@ -189,6 +225,35 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
|
||||
{/* Package contents */}
|
||||
<Box sx={{ px: { xs: 2, sm: 3 }, py: 3 }}>
|
||||
{itemizedUnavailable ? (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
textAlign: 'center',
|
||||
bgcolor: 'var(--fa-color-surface-warm, #FEF9F5)',
|
||||
borderRadius: 'var(--fa-border-radius-md, 8px)',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
px: 3,
|
||||
py: 4,
|
||||
}}
|
||||
>
|
||||
<InfoOutlinedIcon
|
||||
sx={{ fontSize: 28, color: 'var(--fa-color-brand-500)', mb: 1.5 }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="h6" component="p" sx={{ mb: 1 }}>
|
||||
Itemised Pricing Unavailable
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ maxWidth: 360 }}>
|
||||
This provider has shared their overall package price, but has not provided a
|
||||
detailed breakdown of what is included.
|
||||
</Typography>
|
||||
</Box>
|
||||
) : (
|
||||
<>
|
||||
{/* Main sections — included in the package price */}
|
||||
{sections.map((section, idx) => (
|
||||
<Box key={section.heading} sx={{ mb: idx < sections.length - 1 ? 3 : 0 }}>
|
||||
@@ -209,6 +274,8 @@ export const PackageDetail = React.forwardRef<HTMLDivElement, PackageDetailProps
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* Terms & Conditions footer */}
|
||||
|
||||
@@ -5,7 +5,6 @@ import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
|
||||
import { HomePage } from './HomePage';
|
||||
import type { FeaturedProvider, TrustStat } from './HomePage';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import { Footer } from '../../organisms/Footer';
|
||||
|
||||
@@ -210,9 +209,8 @@ const faqItems = [
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof HomePage> = {
|
||||
title: 'Pages/HomePage',
|
||||
title: 'Archive/HomePage V1',
|
||||
component: HomePage,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
@@ -265,68 +263,3 @@ export const Mobile: Story = {
|
||||
viewport: { defaultViewport: 'mobile1' },
|
||||
},
|
||||
};
|
||||
|
||||
// ─── V2 data ────────────────────────────────────────────────────────────────
|
||||
|
||||
const trustStats: TrustStat[] = [
|
||||
{ value: '1,500+', label: 'Families helped' },
|
||||
{ value: '4.9', label: 'Google Rating' },
|
||||
{ value: '300+', label: 'Funeral directors' },
|
||||
];
|
||||
|
||||
const featuredProviders: FeaturedProvider[] = [
|
||||
{
|
||||
id: 'parsons',
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wentworth, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/E8E0D6/8B6F47?text=H.Parsons',
|
||||
logoUrl: 'https://placehold.co/64x64/FEF9F5/BA834E?text=HP',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
startingPrice: 900,
|
||||
},
|
||||
{
|
||||
id: 'rankins',
|
||||
name: 'Rankins Funeral Services',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/D7E1E2/4C5B6B?text=Rankins',
|
||||
logoUrl: 'https://placehold.co/64x64/F2F5F6/4C5B6B?text=R',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
startingPrice: 1200,
|
||||
},
|
||||
{
|
||||
id: 'easy-funerals',
|
||||
name: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/F0F7F0/3B7A3B?text=Easy+Funerals',
|
||||
logoUrl: 'https://placehold.co/64x64/F0F7F0/3B7A3B?text=EF',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
startingPrice: 850,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── V2 Story ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** V2 layout — full-bleed hero, stats bar, map + provider cards, editorial testimonials */
|
||||
export const V2: Story = {
|
||||
args: {
|
||||
navigation: nav,
|
||||
footer,
|
||||
heroImageUrl: '/brandassets/images/heroes/parsonshero.png',
|
||||
stats: trustStats,
|
||||
featuredProviders,
|
||||
onSelectFeaturedProvider: (id) => console.log('Featured provider:', id),
|
||||
features,
|
||||
googleRating: 4.9,
|
||||
googleReviewCount: 2340,
|
||||
testimonials,
|
||||
faqItems,
|
||||
onSearch: (params) => console.log('Search:', params),
|
||||
onCtaClick: () => console.log('CTA clicked'),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,8 +12,8 @@ import StarBorderIcon from '@mui/icons-material/StarBorder';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Button } from '../../atoms/Button';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
import { FuneralFinderV3, type FuneralFinderV3SearchParams } from '../../organisms/FuneralFinder';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
@@ -80,9 +80,11 @@ export interface HomePageProps {
|
||||
/** Hero background image URL (full-bleed layout — V2). Takes priority over heroImage. */
|
||||
heroImageUrl?: string;
|
||||
|
||||
/** FuneralFinder search callback */
|
||||
/** Override the default FuneralFinder widget with a custom element */
|
||||
finderSlot?: React.ReactNode;
|
||||
/** FuneralFinder search callback (used when finderSlot is not provided) */
|
||||
onSearch?: (params: FuneralFinderV3SearchParams) => void;
|
||||
/** FuneralFinder loading state */
|
||||
/** FuneralFinder loading state (used when finderSlot is not provided) */
|
||||
searchLoading?: boolean;
|
||||
|
||||
/** Trust stats bar (e.g. "1,500+ families helped") */
|
||||
@@ -102,6 +104,10 @@ export interface HomePageProps {
|
||||
|
||||
/** Feature cards for "Why Use FA" */
|
||||
features?: FeatureCard[];
|
||||
/** Features section heading */
|
||||
featuresHeading?: string;
|
||||
/** Features section body text */
|
||||
featuresBody?: string;
|
||||
|
||||
/** Aggregate Google rating */
|
||||
googleRating?: number;
|
||||
@@ -170,6 +176,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
heroSubheading = "Funeral planning doesn't have to be overwhelming. Whether you're thinking ahead or arranging for a loved one, find trusted local providers with transparent pricing, all at your own pace.",
|
||||
heroImage,
|
||||
heroImageUrl,
|
||||
finderSlot,
|
||||
onSearch,
|
||||
searchLoading,
|
||||
partnerLogos = [],
|
||||
@@ -178,6 +185,8 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
discoverMapSlot,
|
||||
onSelectFeaturedProvider,
|
||||
features = [],
|
||||
featuresHeading = 'How it works',
|
||||
featuresBody = 'Search local funeral directors, compare transparent pricing, and personalise a plan — all in your own time. No pressure, no hidden costs.',
|
||||
googleRating,
|
||||
googleReviewCount,
|
||||
testimonials = [],
|
||||
@@ -215,8 +224,9 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
position: 'relative',
|
||||
minHeight: { xs: 480, md: 640 },
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
justifyContent: 'flex-start',
|
||||
backgroundImage: `url(${heroImageUrl})`,
|
||||
backgroundSize: 'cover',
|
||||
backgroundPosition: 'center',
|
||||
@@ -225,20 +235,26 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
background:
|
||||
'linear-gradient(180deg, rgba(0,0,0,0.35) 0%, rgba(0,0,0,0.15) 50%, rgba(0,0,0,0.4) 100%)',
|
||||
'linear-gradient(180deg, rgba(0,0,0,0.55) 0%, rgba(0,0,0,0.1) 60%, rgba(0,0,0,0.3) 100%)',
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Container
|
||||
maxWidth="md"
|
||||
sx={{ position: 'relative', zIndex: 1, textAlign: 'center', py: 6 }}
|
||||
sx={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
textAlign: 'center',
|
||||
pt: { xs: 8, md: 11 },
|
||||
pb: 4,
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="display3"
|
||||
component="h1"
|
||||
id="hero-heading"
|
||||
tabIndex={-1}
|
||||
sx={{ mb: 2, color: 'var(--fa-color-white)' }}
|
||||
sx={{ mb: 3, color: 'var(--fa-color-white)' }}
|
||||
>
|
||||
{heroHeading}
|
||||
</Typography>
|
||||
@@ -249,6 +265,29 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
{heroSubheading}
|
||||
</Typography>
|
||||
</Container>
|
||||
|
||||
{/* FuneralFinder — inside the hero, bottom portion overlaps into white */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
zIndex: 2,
|
||||
width: '100%',
|
||||
px: 2,
|
||||
pt: 2,
|
||||
pb: 0,
|
||||
mb: { xs: -14, md: -18 },
|
||||
}}
|
||||
>
|
||||
<Box sx={{ width: '100%', maxWidth: finderSlot ? 500 : 520, mx: 'auto' }}>
|
||||
{finderSlot || (
|
||||
<FuneralFinderV3
|
||||
heading="Find your local providers"
|
||||
onSearch={onSearch}
|
||||
loading={searchLoading}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
) : (
|
||||
/* ── V1: Split hero ── */
|
||||
@@ -316,25 +355,29 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
Section 2: FuneralFinder Widget (overlapping card)
|
||||
Section 2: FuneralFinder Widget (V1 only — overlapping card)
|
||||
V2 renders the finder inside the hero section above.
|
||||
═══════════════════════════════════════════════════════════════════ */}
|
||||
{!isFullBleedHero && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
zIndex: 1,
|
||||
mt: { xs: -6, md: -10 },
|
||||
mt: { xs: -4, md: -6 },
|
||||
px: 2,
|
||||
}}
|
||||
>
|
||||
<Box sx={{ maxWidth: 520, mx: 'auto' }}>
|
||||
<Box sx={{ maxWidth: 620, mx: 'auto' }}>
|
||||
{finderSlot || (
|
||||
<FuneralFinderV3
|
||||
heading="Find funeral providers near you"
|
||||
subheading="Tell us a little about what you're looking for and we'll show you options in your area."
|
||||
heading="Find your local providers"
|
||||
onSearch={onSearch}
|
||||
loading={searchLoading}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* ═══════════════════════════════════════════════════════════════════
|
||||
Section 2c: Discover — Map + Featured Providers (V2)
|
||||
@@ -345,16 +388,17 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
aria-labelledby="discover-heading"
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||
py: { xs: 6, md: 10 },
|
||||
pt: { xs: 22, md: 28 },
|
||||
pb: { xs: 8, md: 12 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ textAlign: 'center', mb: { xs: 4, md: 6 } }}>
|
||||
<Box sx={{ textAlign: 'center', mb: { xs: 5, md: 8 } }}>
|
||||
<Typography
|
||||
variant="display3"
|
||||
component="h2"
|
||||
id="discover-heading"
|
||||
sx={{ mb: 1.5, color: 'text.primary' }}
|
||||
sx={{ mb: 2, color: 'text.primary' }}
|
||||
>
|
||||
See what you'll discover
|
||||
</Typography>
|
||||
@@ -373,27 +417,41 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
display: 'grid',
|
||||
gridTemplateColumns: { xs: '1fr', md: '1fr 1fr' },
|
||||
gap: 3,
|
||||
alignItems: 'start',
|
||||
alignItems: 'stretch',
|
||||
}}
|
||||
>
|
||||
{/* Map placeholder */}
|
||||
{/* Map — fills the grid cell to match card stack height */}
|
||||
<Box
|
||||
sx={{
|
||||
position: 'relative',
|
||||
borderRadius: 2,
|
||||
overflow: 'hidden',
|
||||
minHeight: { xs: 240, md: 400 },
|
||||
minHeight: { xs: 240, md: 0 },
|
||||
bgcolor: 'var(--fa-color-surface-cool)',
|
||||
border: '1px solid',
|
||||
borderColor: 'divider',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'& > img, & > div': {
|
||||
position: { md: 'absolute' },
|
||||
inset: { md: 0 },
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
objectFit: 'cover',
|
||||
},
|
||||
}}
|
||||
>
|
||||
{discoverMapSlot || (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
}}
|
||||
>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Map coming soon
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -416,6 +474,13 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* CTA */}
|
||||
<Box sx={{ textAlign: 'center', mt: 4 }}>
|
||||
<Button variant="text" size="medium" onClick={onCtaClick}>
|
||||
Start exploring →
|
||||
</Button>
|
||||
</Box>
|
||||
</Container>
|
||||
</Box>
|
||||
)}
|
||||
@@ -429,15 +494,15 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
aria-label="Trusted partners"
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-cool)',
|
||||
pt: { xs: 8, md: 10 },
|
||||
pb: { xs: 6, md: 8 },
|
||||
pt: { xs: 10, md: 13 },
|
||||
pb: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ textAlign: 'center', mb: { xs: 3, md: 4 } }}
|
||||
sx={{ textAlign: 'center', mb: { xs: 4, md: 6 } }}
|
||||
>
|
||||
{partnerTrustLine}
|
||||
</Typography>
|
||||
@@ -474,7 +539,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
aria-label="Partner funeral directors"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
gap: { xs: 6, md: 8 },
|
||||
gap: { xs: 8, md: 12 },
|
||||
alignItems: 'center',
|
||||
width: 'max-content',
|
||||
animation: 'logoScroll 35s linear infinite',
|
||||
@@ -494,13 +559,12 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
alt={i < partnerLogos.length ? logo.alt : ''}
|
||||
aria-hidden={i >= partnerLogos.length ? true : undefined}
|
||||
sx={{
|
||||
height: { xs: 48, md: 64 },
|
||||
height: { xs: 46, md: 55 },
|
||||
maxWidth: { xs: 140, md: 184 },
|
||||
width: 'auto',
|
||||
objectFit: 'contain',
|
||||
filter: 'grayscale(100%)',
|
||||
opacity: 0.5,
|
||||
transition: 'opacity 0.2s, filter 0.2s',
|
||||
'&:hover': { filter: 'grayscale(0%)', opacity: 1 },
|
||||
filter: 'grayscale(100%) brightness(1.2)',
|
||||
opacity: 0.4,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
/>
|
||||
@@ -519,34 +583,25 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
aria-labelledby="features-heading"
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-default)',
|
||||
py: { xs: 6, md: 10 },
|
||||
py: { xs: 8, md: 12 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
<Box sx={{ textAlign: 'center', mb: { xs: 4, md: 6 } }}>
|
||||
<Box sx={{ textAlign: 'center', mb: { xs: 5, md: 8 } }}>
|
||||
<Typography
|
||||
variant="overline"
|
||||
sx={{ color: 'var(--fa-color-text-brand)', mb: 1.5, display: 'block' }}
|
||||
>
|
||||
Why use Funeral Arranger
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="h2"
|
||||
variant="display3"
|
||||
component="h2"
|
||||
id="features-heading"
|
||||
sx={{ mb: 2, color: 'text.primary' }}
|
||||
sx={{ mb: 2.5, color: 'text.primary' }}
|
||||
>
|
||||
Making an impossible time a little easier
|
||||
{featuresHeading}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="body2"
|
||||
variant="body1"
|
||||
color="text.secondary"
|
||||
sx={{ maxWidth: 560, mx: 'auto' }}
|
||||
>
|
||||
Funeral planning doesn't have to be overwhelming. Whether a loved one has
|
||||
just passed, is imminent, or you're pre-planning for the future. Compare
|
||||
transparent pricing from local funeral directors. Explore the service options,
|
||||
coffins and more to personalise a funeral plan in clear, easy steps.
|
||||
{featuresBody}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
@@ -558,41 +613,27 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
sm: 'repeat(2, 1fr)',
|
||||
md: 'repeat(4, 1fr)',
|
||||
},
|
||||
gap: { xs: 3, md: 3 },
|
||||
gap: { xs: 4, md: 5 },
|
||||
}}
|
||||
>
|
||||
{features.map((feature) => (
|
||||
<Card key={feature.heading} variant="outlined" padding="compact">
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Box key={feature.heading} sx={{ textAlign: 'center' }}>
|
||||
<Box
|
||||
sx={{
|
||||
mb: 1.5,
|
||||
mx: 'auto',
|
||||
width: 56,
|
||||
height: 56,
|
||||
borderRadius: '50%',
|
||||
bgcolor: 'var(--fa-color-brand-50)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
mb: 2,
|
||||
color: 'primary.main',
|
||||
'& svg': { fontSize: 28 },
|
||||
'& svg': { fontSize: 32 },
|
||||
}}
|
||||
>
|
||||
{feature.icon}
|
||||
</Box>
|
||||
<Typography
|
||||
variant="h5"
|
||||
component="h3"
|
||||
sx={{ mb: 1, color: 'text.primary' }}
|
||||
>
|
||||
<Typography variant="h6" component="h3" sx={{ mb: 1, color: 'text.primary' }}>
|
||||
{feature.heading}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{feature.description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</Container>
|
||||
@@ -607,18 +648,18 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
component="section"
|
||||
aria-labelledby="reviews-heading"
|
||||
sx={{
|
||||
py: { xs: 6, md: 10 },
|
||||
bgcolor: 'var(--fa-color-surface-warm)',
|
||||
py: { xs: 8, md: 12 },
|
||||
bgcolor: 'var(--fa-color-surface-subtle)',
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="md">
|
||||
<Typography
|
||||
variant="h2"
|
||||
variant="display3"
|
||||
component="h2"
|
||||
id="reviews-heading"
|
||||
sx={{ textAlign: 'center', mb: 1, color: 'text.primary' }}
|
||||
>
|
||||
Testimonials
|
||||
What families are saying
|
||||
</Typography>
|
||||
|
||||
{googleRating != null && (
|
||||
@@ -642,13 +683,14 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
</Box>
|
||||
)}
|
||||
|
||||
{/* Editorial testimonials — alternating alignment */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
|
||||
{/* Editorial testimonials — alternating alignment with dividers */}
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 0 }}>
|
||||
{testimonials.map((t, i) => {
|
||||
const isRight = i % 2 === 1;
|
||||
return (
|
||||
<React.Fragment key={`${t.name}-${i}`}>
|
||||
{i > 0 && <Divider sx={{ my: 4 }} />}
|
||||
<Box
|
||||
key={`${t.name}-${i}`}
|
||||
sx={{
|
||||
textAlign: isRight ? 'right' : 'left',
|
||||
maxWidth: '85%',
|
||||
@@ -691,6 +733,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</React.Fragment>
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
@@ -705,20 +748,21 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
component="section"
|
||||
aria-labelledby="cta-heading"
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-warm)',
|
||||
py: { xs: 6, md: 10 },
|
||||
background:
|
||||
'linear-gradient(180deg, var(--fa-color-brand-100, #F5EDE4) 0%, var(--fa-color-surface-warm, #FEF9F5) 100%)',
|
||||
py: { xs: 8, md: 10 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg" sx={{ textAlign: 'center' }}>
|
||||
<Container maxWidth="md" sx={{ textAlign: 'center' }}>
|
||||
<Typography
|
||||
variant="displaySm"
|
||||
variant="display3"
|
||||
component="h2"
|
||||
id="cta-heading"
|
||||
sx={{ mb: 3, color: 'text.primary', maxWidth: 500, mx: 'auto' }}
|
||||
sx={{ mb: 3, color: 'text.primary', whiteSpace: { md: 'nowrap' } }}
|
||||
>
|
||||
{ctaHeading}
|
||||
</Typography>
|
||||
<Button variant="contained" size="large" onClick={onCtaClick}>
|
||||
<Button variant="text" size="large" onClick={onCtaClick}>
|
||||
{ctaButtonLabel}
|
||||
</Button>
|
||||
</Container>
|
||||
@@ -733,7 +777,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
aria-labelledby="faq-heading"
|
||||
sx={{
|
||||
bgcolor: 'var(--fa-color-surface-default)',
|
||||
py: { xs: 6, md: 10 },
|
||||
py: { xs: 8, md: 12 },
|
||||
}}
|
||||
>
|
||||
<Container maxWidth="lg">
|
||||
@@ -741,7 +785,7 @@ export const HomePage = React.forwardRef<HTMLDivElement, HomePageProps>(
|
||||
variant="h2"
|
||||
component="h2"
|
||||
id="faq-heading"
|
||||
sx={{ textAlign: 'center', mb: { xs: 4, md: 6 }, color: 'text.primary' }}
|
||||
sx={{ textAlign: 'center', mb: { xs: 5, md: 8 }, color: 'text.primary' }}
|
||||
>
|
||||
FAQ
|
||||
</Typography>
|
||||
|
||||
246
src/components/pages/HomePage/HomePageV2.stories.tsx
Normal file
246
src/components/pages/HomePage/HomePageV2.stories.tsx
Normal file
@@ -0,0 +1,246 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
|
||||
import { HomePage } from './HomePage';
|
||||
import type { FeaturedProvider, TrustStat } from './HomePage';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import { Footer } from '../../organisms/Footer';
|
||||
|
||||
// ─── Shared helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
const FALogo = () => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||
/>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-short.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const FALogoInverse = () => (
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 24, filter: 'brightness(0) invert(1)', opacity: 0.9 }}
|
||||
/>
|
||||
);
|
||||
|
||||
const nav = (
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<Footer
|
||||
logo={<FALogoInverse />}
|
||||
tagline="Helping Australian families plan with confidence"
|
||||
linkGroups={[
|
||||
{
|
||||
heading: 'Services',
|
||||
links: [
|
||||
{ label: 'Find a Director', href: '/directors' },
|
||||
{ label: 'Compare Venues', href: '/venues' },
|
||||
{ label: 'Pricing Guide', href: '/pricing' },
|
||||
{ label: 'Start Planning', href: '/arrange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Support',
|
||||
links: [
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Grief Resources', href: '/resources' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Company',
|
||||
links: [
|
||||
{ label: 'About Us', href: '/about' },
|
||||
{ label: 'Provider Portal', href: '/provider-portal' },
|
||||
{ label: 'Partner With Us', href: '/partners' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
phone="1800 987 888"
|
||||
email="support@funeralarranger.com.au"
|
||||
legalLinks={[
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
{ label: 'Accessibility', href: '/accessibility' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
// ─── Data ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <VerifiedOutlinedIcon />,
|
||||
heading: 'Transparent, verified pricing',
|
||||
description:
|
||||
'All costs are itemised for verified partners. No surprise fees. See pricing and options before you commit.',
|
||||
},
|
||||
{
|
||||
icon: <AccessTimeIcon />,
|
||||
heading: 'Available 24 hours a day',
|
||||
description:
|
||||
'Compare, plan and arrange at your own pace, day or night. No pressure to commit online.',
|
||||
},
|
||||
{
|
||||
icon: <SearchIcon />,
|
||||
heading: 'Find local & compare',
|
||||
description: 'Search and compare local funeral directors to find the right choice for you.',
|
||||
},
|
||||
{
|
||||
icon: <SupportAgentOutlinedIcon />,
|
||||
heading: 'Support when you need it',
|
||||
description:
|
||||
'Arrange everything online or be guided through the steps by your preferred funeral director.',
|
||||
},
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: 'Sarah H.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'At the most difficult time in our lives, this site made comparing costs so straightforward. We saved over $800.',
|
||||
timeAgo: '3 weeks ago',
|
||||
},
|
||||
{
|
||||
name: 'James M.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'The itemised quote builder meant we could personalise the service within our budget. Highly recommended.',
|
||||
timeAgo: '1 month ago',
|
||||
},
|
||||
{
|
||||
name: 'Tracy W.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'I had no idea there was such a price difference between local directors. This saved us from overpaying.',
|
||||
timeAgo: '2 months ago',
|
||||
},
|
||||
];
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: 'What is Funeral Arranger?',
|
||||
answer:
|
||||
'Funeral Arranger is an online platform that helps Australian families find, compare and arrange funeral services. We connect you with trusted local funeral directors and provide transparent pricing so you can make informed decisions during a difficult time.',
|
||||
},
|
||||
{
|
||||
question: 'What makes Funeral Arranger different from other funeral service providers?',
|
||||
answer:
|
||||
'Unlike traditional funeral homes, we are an independent comparison platform. We show you transparent, itemised pricing from multiple verified providers in your area so you can compare options and choose what is right for your family and budget.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to complete all steps at once?',
|
||||
answer:
|
||||
'No. You can save your progress at any time and return when you are ready. Whether you are pre-planning or arranging at short notice, the process works at your pace with no time pressure.',
|
||||
},
|
||||
{
|
||||
question: 'How much does a funeral cost in Australia?',
|
||||
answer:
|
||||
'Funeral costs in Australia typically range from $4,000 for a simple cremation to $15,000 or more for a full traditional service. Costs vary by location, provider, and the options you choose. Our platform helps you compare real prices from local providers.',
|
||||
},
|
||||
{
|
||||
question: 'What is the cheapest funeral option?',
|
||||
answer:
|
||||
'A direct cremation (no service) is generally the most affordable option, starting from around $2,000\u2013$4,000 depending on your location. Our platform shows you all available options so you can find the right balance of service and cost.',
|
||||
},
|
||||
];
|
||||
|
||||
const trustStats: TrustStat[] = [
|
||||
{ value: '1,500+', label: 'Families helped' },
|
||||
{ value: '4.9', label: 'Google Rating' },
|
||||
{ value: '300+', label: 'Funeral directors' },
|
||||
];
|
||||
|
||||
const featuredProviders: FeaturedProvider[] = [
|
||||
{
|
||||
id: 'parsons',
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wentworth, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/E8E0D6/8B6F47?text=H.Parsons',
|
||||
logoUrl: 'https://placehold.co/64x64/FEF9F5/BA834E?text=HP',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
startingPrice: 900,
|
||||
},
|
||||
{
|
||||
id: 'rankins',
|
||||
name: 'Rankins Funeral Services',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/D7E1E2/4C5B6B?text=Rankins',
|
||||
logoUrl: 'https://placehold.co/64x64/F2F5F6/4C5B6B?text=R',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
startingPrice: 1200,
|
||||
},
|
||||
{
|
||||
id: 'easy-funerals',
|
||||
name: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
verified: true,
|
||||
imageUrl: 'https://placehold.co/600x200/F0F7F0/3B7A3B?text=Easy+Funerals',
|
||||
logoUrl: 'https://placehold.co/64x64/F0F7F0/3B7A3B?text=EF',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
startingPrice: 850,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof HomePage> = {
|
||||
title: 'Archive/HomePage V2',
|
||||
component: HomePage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HomePage>;
|
||||
|
||||
// ─── Stories ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** V2 layout — full-bleed hero, stats bar, map + provider cards, editorial testimonials */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
navigation: nav,
|
||||
footer,
|
||||
heroImageUrl: '/brandassets/images/heroes/parsonshero.png',
|
||||
stats: trustStats,
|
||||
featuredProviders,
|
||||
onSelectFeaturedProvider: (id) => console.log('Featured provider:', id),
|
||||
features,
|
||||
googleRating: 4.9,
|
||||
googleReviewCount: 2340,
|
||||
testimonials,
|
||||
faqItems,
|
||||
onSearch: (params) => console.log('Search:', params),
|
||||
onCtaClick: () => console.log('CTA clicked'),
|
||||
},
|
||||
};
|
||||
281
src/components/pages/HomePage/HomePageV3.stories.tsx
Normal file
281
src/components/pages/HomePage/HomePageV3.stories.tsx
Normal file
@@ -0,0 +1,281 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
|
||||
import { HomePage } from './HomePage';
|
||||
import type { FeaturedProvider, TrustStat, PartnerLogo } from './HomePage';
|
||||
import React from 'react';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import { Footer } from '../../organisms/Footer';
|
||||
|
||||
// ─── Shared helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
const FALogo = () => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||
/>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-short.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const FALogoFooter = () => (
|
||||
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 24 }} />
|
||||
);
|
||||
|
||||
const nav = (
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<Footer
|
||||
logo={<FALogoFooter />}
|
||||
tagline="Helping Australian families plan with confidence"
|
||||
linkGroups={[
|
||||
{
|
||||
heading: 'Services',
|
||||
links: [
|
||||
{ label: 'Find a Director', href: '/directors' },
|
||||
{ label: 'Compare Venues', href: '/venues' },
|
||||
{ label: 'Pricing Guide', href: '/pricing' },
|
||||
{ label: 'Start Planning', href: '/arrange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Support',
|
||||
links: [
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Grief Resources', href: '/resources' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Company',
|
||||
links: [
|
||||
{ label: 'About Us', href: '/about' },
|
||||
{ label: 'Provider Portal', href: '/provider-portal' },
|
||||
{ label: 'Partner With Us', href: '/partners' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
phone="1800 987 888"
|
||||
email="support@funeralarranger.com.au"
|
||||
legalLinks={[
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
{ label: 'Accessibility', href: '/accessibility' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
// ─── Data ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <VerifiedOutlinedIcon />,
|
||||
heading: 'Transparent, verified pricing',
|
||||
description:
|
||||
'All costs are itemised for verified partners. No surprise fees. See pricing and options before you commit.',
|
||||
},
|
||||
{
|
||||
icon: <AccessTimeIcon />,
|
||||
heading: 'Available 24 hours a day',
|
||||
description:
|
||||
'Compare, plan and arrange at your own pace, day or night. No pressure to commit online.',
|
||||
},
|
||||
{
|
||||
icon: <SearchIcon />,
|
||||
heading: 'Find local & compare',
|
||||
description: 'Search and compare local funeral directors to find the right choice for you.',
|
||||
},
|
||||
{
|
||||
icon: <SupportAgentOutlinedIcon />,
|
||||
heading: 'Support when you need it',
|
||||
description:
|
||||
'Arrange everything online or be guided through the steps by your preferred funeral director.',
|
||||
},
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: 'Sarah H.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'At the most difficult time in our lives, this site made comparing costs so straightforward. We saved over $800.',
|
||||
timeAgo: '3 weeks ago',
|
||||
},
|
||||
{
|
||||
name: 'James M.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'The itemised quote builder meant we could personalise the service within our budget. Highly recommended.',
|
||||
timeAgo: '1 month ago',
|
||||
},
|
||||
{
|
||||
name: 'Tracy W.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'I had no idea there was such a price difference between local directors. This saved us from overpaying.',
|
||||
timeAgo: '2 months ago',
|
||||
},
|
||||
];
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: 'What is Funeral Arranger?',
|
||||
answer:
|
||||
'Funeral Arranger is an online platform that helps Australian families find, compare and arrange funeral services. We connect you with trusted local funeral directors and provide transparent pricing so you can make informed decisions during a difficult time.',
|
||||
},
|
||||
{
|
||||
question: 'What makes Funeral Arranger different from other funeral service providers?',
|
||||
answer:
|
||||
'Unlike traditional funeral homes, we are an independent comparison platform. We show you transparent, itemised pricing from multiple verified providers in your area so you can compare options and choose what is right for your family and budget.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to complete all steps at once?',
|
||||
answer:
|
||||
'No. You can save your progress at any time and return when you are ready. Whether you are pre-planning or arranging at short notice, the process works at your pace with no time pressure.',
|
||||
},
|
||||
{
|
||||
question: 'How much does a funeral cost in Australia?',
|
||||
answer:
|
||||
'Funeral costs in Australia typically range from $4,000 for a simple cremation to $15,000 or more for a full traditional service. Costs vary by location, provider, and the options you choose. Our platform helps you compare real prices from local providers.',
|
||||
},
|
||||
{
|
||||
question: 'What is the cheapest funeral option?',
|
||||
answer:
|
||||
'A direct cremation (no service) is generally the most affordable option, starting from around $2,000\u2013$4,000 depending on your location. Our platform shows you all available options so you can find the right balance of service and cost.',
|
||||
},
|
||||
];
|
||||
|
||||
const trustStats: TrustStat[] = [
|
||||
{ value: '1,500+', label: 'Families helped' },
|
||||
{ value: '4.9', label: 'Google Rating' },
|
||||
{ value: '300+', label: 'Funeral directors' },
|
||||
];
|
||||
|
||||
const featuredProviders: FeaturedProvider[] = [
|
||||
{
|
||||
id: 'parsons',
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/hparsons-funeral-home-kiama/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/hparsons-funeral-directors/logo.png',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
startingPrice: 900,
|
||||
},
|
||||
{
|
||||
id: 'rankins',
|
||||
name: 'Rankins Funerals',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/rankins-funeral-home-warrawong/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/rankins-funerals/logo.png',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
startingPrice: 1200,
|
||||
},
|
||||
{
|
||||
id: 'easy-funerals',
|
||||
name: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/lakeside-memorial-park-chapel/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/easy-funerals/logo.png',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
startingPrice: 850,
|
||||
},
|
||||
];
|
||||
|
||||
const partnerLogos: PartnerLogo[] = [
|
||||
{
|
||||
src: '/brandassets/images/providers/hparsons-funeral-directors/logo.png',
|
||||
alt: 'H.Parsons Funeral Directors',
|
||||
},
|
||||
{ src: '/brandassets/images/providers/rankins-funerals/logo.png', alt: 'Rankins Funerals' },
|
||||
{ src: '/brandassets/images/providers/easy-funerals/logo.png', alt: 'Easy Funerals' },
|
||||
{ src: '/brandassets/images/providers/lady-anne-funerals/logo.png', alt: 'Lady Anne Funerals' },
|
||||
{
|
||||
src: '/brandassets/images/providers/killick-family-funerals/logo.png',
|
||||
alt: 'Killick Family Funerals',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/kenneallys-funerals/logo.png',
|
||||
alt: "Kenneally's Funerals",
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/wollongong-city-funerals/logo.png',
|
||||
alt: 'Wollongong City Funerals',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/hparsons-funeral-directors-shoalhaven/logo.png',
|
||||
alt: 'H.Parsons Shoalhaven',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/mackay-family-funerals/logo.webp',
|
||||
alt: 'Mackay Family Funerals',
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof HomePage> = {
|
||||
title: 'Archive/HomePage V3',
|
||||
component: HomePage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HomePage>;
|
||||
|
||||
// ─── Stories ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** V3 layout — hero-3 background, updated copy, bullet-point subheading */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
navigation: nav,
|
||||
footer,
|
||||
heroImageUrl: '/brandassets/images/heroes/hero-3.png',
|
||||
heroHeading: 'Compare funeral directors pricing near you and arrange with confidence',
|
||||
heroSubheading: 'Transparent pricing \u00B7 No hidden fees \u00B7 Arrange 24/7',
|
||||
stats: trustStats,
|
||||
featuredProviders,
|
||||
discoverMapSlot: React.createElement('img', {
|
||||
src: '/brandassets/images/placeholder/map.png',
|
||||
alt: 'Map showing provider locations',
|
||||
style: { width: '100%', height: '100%', objectFit: 'cover' },
|
||||
}),
|
||||
onSelectFeaturedProvider: (id) => console.log('Featured provider:', id),
|
||||
partnerLogos,
|
||||
partnerTrustLine: 'Trusted by hundreds of verified funeral directors across Australia',
|
||||
features,
|
||||
googleRating: 4.9,
|
||||
googleReviewCount: 2340,
|
||||
testimonials,
|
||||
faqItems,
|
||||
onSearch: (params) => console.log('Search:', params),
|
||||
onCtaClick: () => console.log('CTA clicked'),
|
||||
},
|
||||
};
|
||||
285
src/components/pages/HomePage/HomePageV4.stories.tsx
Normal file
285
src/components/pages/HomePage/HomePageV4.stories.tsx
Normal file
@@ -0,0 +1,285 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import Box from '@mui/material/Box';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import AccessTimeIcon from '@mui/icons-material/AccessTime';
|
||||
import SearchIcon from '@mui/icons-material/Search';
|
||||
import SupportAgentOutlinedIcon from '@mui/icons-material/SupportAgentOutlined';
|
||||
import { HomePage } from './HomePage';
|
||||
import type { FeaturedProvider, TrustStat, PartnerLogo } from './HomePage';
|
||||
import { FuneralFinderV4 } from '../../organisms/FuneralFinder/FuneralFinderV4';
|
||||
import React from 'react';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import { Footer } from '../../organisms/Footer';
|
||||
|
||||
// ─── Shared helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
const FALogo = () => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||
/>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-short.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const FALogoFooter = () => (
|
||||
<Box component="img" src="/brandlogo/logo-full.svg" alt="Funeral Arranger" sx={{ height: 24 }} />
|
||||
);
|
||||
|
||||
const nav = (
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const footer = (
|
||||
<Footer
|
||||
logo={<FALogoFooter />}
|
||||
tagline="Helping Australian families plan with confidence"
|
||||
linkGroups={[
|
||||
{
|
||||
heading: 'Services',
|
||||
links: [
|
||||
{ label: 'Find a Director', href: '/directors' },
|
||||
{ label: 'Compare Venues', href: '/venues' },
|
||||
{ label: 'Pricing Guide', href: '/pricing' },
|
||||
{ label: 'Start Planning', href: '/arrange' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Support',
|
||||
links: [
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Grief Resources', href: '/resources' },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Company',
|
||||
links: [
|
||||
{ label: 'About Us', href: '/about' },
|
||||
{ label: 'Provider Portal', href: '/provider-portal' },
|
||||
{ label: 'Partner With Us', href: '/partners' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
phone="1800 987 888"
|
||||
email="support@funeralarranger.com.au"
|
||||
legalLinks={[
|
||||
{ label: 'Privacy Policy', href: '/privacy' },
|
||||
{ label: 'Terms of Service', href: '/terms' },
|
||||
{ label: 'Accessibility', href: '/accessibility' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
// ─── Data ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const features = [
|
||||
{
|
||||
icon: <VerifiedOutlinedIcon />,
|
||||
heading: 'Transparent, verified pricing',
|
||||
description:
|
||||
'All costs are itemised for verified partners. No surprise fees. See pricing and options before you commit.',
|
||||
},
|
||||
{
|
||||
icon: <AccessTimeIcon />,
|
||||
heading: 'Available 24 hours a day',
|
||||
description:
|
||||
'Compare, plan and arrange at your own pace, day or night. No pressure to commit online.',
|
||||
},
|
||||
{
|
||||
icon: <SearchIcon />,
|
||||
heading: 'Find local & compare',
|
||||
description: 'Search and compare local funeral directors to find the right choice for you.',
|
||||
},
|
||||
{
|
||||
icon: <SupportAgentOutlinedIcon />,
|
||||
heading: 'Support when you need it',
|
||||
description:
|
||||
'Arrange everything online or be guided through the steps by your preferred funeral director.',
|
||||
},
|
||||
];
|
||||
|
||||
const testimonials = [
|
||||
{
|
||||
name: 'Sarah H.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'At the most difficult time in our lives, this site made comparing costs so straightforward. We saved over $800.',
|
||||
timeAgo: '3 weeks ago',
|
||||
},
|
||||
{
|
||||
name: 'James M.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'The itemised quote builder meant we could personalise the service within our budget. Highly recommended.',
|
||||
timeAgo: '1 month ago',
|
||||
},
|
||||
{
|
||||
name: 'Tracy W.',
|
||||
rating: 5,
|
||||
quote:
|
||||
'I had no idea there was such a price difference between local directors. This saved us from overpaying.',
|
||||
timeAgo: '2 months ago',
|
||||
},
|
||||
];
|
||||
|
||||
const faqItems = [
|
||||
{
|
||||
question: 'What is Funeral Arranger?',
|
||||
answer:
|
||||
'Funeral Arranger is an online platform that helps Australian families find, compare and arrange funeral services. We connect you with trusted local funeral directors and provide transparent pricing so you can make informed decisions during a difficult time.',
|
||||
},
|
||||
{
|
||||
question: 'What makes Funeral Arranger different from other funeral service providers?',
|
||||
answer:
|
||||
'Unlike traditional funeral homes, we are an independent comparison platform. We show you transparent, itemised pricing from multiple verified providers in your area so you can compare options and choose what is right for your family and budget.',
|
||||
},
|
||||
{
|
||||
question: 'Do I need to complete all steps at once?',
|
||||
answer:
|
||||
'No. You can save your progress at any time and return when you are ready. Whether you are pre-planning or arranging at short notice, the process works at your pace with no time pressure.',
|
||||
},
|
||||
{
|
||||
question: 'How much does a funeral cost in Australia?',
|
||||
answer:
|
||||
'Funeral costs in Australia typically range from $4,000 for a simple cremation to $15,000 or more for a full traditional service. Costs vary by location, provider, and the options you choose. Our platform helps you compare real prices from local providers.',
|
||||
},
|
||||
{
|
||||
question: 'What is the cheapest funeral option?',
|
||||
answer:
|
||||
'A direct cremation (no service) is generally the most affordable option, starting from around $2,000\u2013$4,000 depending on your location. Our platform shows you all available options so you can find the right balance of service and cost.',
|
||||
},
|
||||
];
|
||||
|
||||
const trustStats: TrustStat[] = [
|
||||
{ value: '1,500+', label: 'Families helped' },
|
||||
{ value: '4.9', label: 'Google Rating' },
|
||||
{ value: '300+', label: 'Funeral directors' },
|
||||
];
|
||||
|
||||
const featuredProviders: FeaturedProvider[] = [
|
||||
{
|
||||
id: 'parsons',
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/hparsons-funeral-home-kiama/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/hparsons-funeral-directors/logo.png',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
startingPrice: 900,
|
||||
},
|
||||
{
|
||||
id: 'rankins',
|
||||
name: 'Rankins Funerals',
|
||||
location: 'Wollongong, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/rankins-funeral-home-warrawong/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/rankins-funerals/logo.png',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
startingPrice: 1200,
|
||||
},
|
||||
{
|
||||
id: 'easy-funerals',
|
||||
name: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
verified: true,
|
||||
imageUrl: '/brandassets/images/venues/lakeside-memorial-park-chapel/01.jpg',
|
||||
logoUrl: '/brandassets/images/providers/easy-funerals/logo.png',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
startingPrice: 850,
|
||||
},
|
||||
];
|
||||
|
||||
const partnerLogos: PartnerLogo[] = [
|
||||
{
|
||||
src: '/brandassets/images/providers/hparsons-funeral-directors/logo.png',
|
||||
alt: 'H.Parsons Funeral Directors',
|
||||
},
|
||||
{ src: '/brandassets/images/providers/rankins-funerals/logo.png', alt: 'Rankins Funerals' },
|
||||
{ src: '/brandassets/images/providers/easy-funerals/logo.png', alt: 'Easy Funerals' },
|
||||
{ src: '/brandassets/images/providers/lady-anne-funerals/logo.png', alt: 'Lady Anne Funerals' },
|
||||
{
|
||||
src: '/brandassets/images/providers/killick-family-funerals/logo.png',
|
||||
alt: 'Killick Family Funerals',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/kenneallys-funerals/logo.png',
|
||||
alt: "Kenneally's Funerals",
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/wollongong-city-funerals/logo.png',
|
||||
alt: 'Wollongong City Funerals',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/hparsons-funeral-directors-shoalhaven/logo.png',
|
||||
alt: 'H.Parsons Shoalhaven',
|
||||
},
|
||||
{
|
||||
src: '/brandassets/images/providers/mackay-family-funerals/logo.webp',
|
||||
alt: 'Mackay Family Funerals',
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof HomePage> = {
|
||||
title: 'Archive/HomePage V4',
|
||||
component: HomePage,
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof HomePage>;
|
||||
|
||||
// ─── Stories ────────────────────────────────────────────────────────────────
|
||||
|
||||
/** V4 layout — uses FuneralFinder V4 (stepped form with numbered indicators) */
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
navigation: nav,
|
||||
footer,
|
||||
heroImageUrl: '/brandassets/images/heroes/hero-3.png',
|
||||
heroHeading: 'Compare funeral directors pricing near you and arrange with confidence',
|
||||
heroSubheading: 'Transparent pricing \u00B7 No hidden fees \u00B7 Arrange 24/7',
|
||||
finderSlot: React.createElement(FuneralFinderV4, {
|
||||
onSearch: (params) => console.log('Search:', params),
|
||||
}),
|
||||
stats: trustStats,
|
||||
featuredProviders,
|
||||
discoverMapSlot: React.createElement('img', {
|
||||
src: '/brandassets/images/placeholder/map.png',
|
||||
alt: 'Map showing provider locations',
|
||||
style: { width: '100%', height: '100%', objectFit: 'cover' },
|
||||
}),
|
||||
onSelectFeaturedProvider: (id) => console.log('Featured provider:', id),
|
||||
partnerLogos,
|
||||
partnerTrustLine: 'Trusted by hundreds of verified funeral directors across Australia',
|
||||
features,
|
||||
googleRating: 4.9,
|
||||
googleReviewCount: 2340,
|
||||
testimonials,
|
||||
faqItems,
|
||||
onSearch: (params) => console.log('Search:', params),
|
||||
onCtaClick: () => console.log('CTA clicked'),
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,206 @@
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { UnverifiedPackageT2 } from './UnverifiedPackageT2';
|
||||
import type {
|
||||
UnverifiedPackageT2Data,
|
||||
UnverifiedPackageT2Provider,
|
||||
NearbyVerifiedPackage,
|
||||
} from './UnverifiedPackageT2';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const FALogo = () => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||
/>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-short.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const nav = (
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const mockProvider: UnverifiedPackageT2Provider = {
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wentworth, NSW',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
};
|
||||
|
||||
const mockPackages: UnverifiedPackageT2Data[] = [
|
||||
{
|
||||
id: 'everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 2700,
|
||||
description:
|
||||
'A funeral service at a chapel or church with a funeral procession, including commonly selected options.',
|
||||
},
|
||||
{
|
||||
id: 'deluxe',
|
||||
name: 'Deluxe Funeral Package',
|
||||
price: 4900,
|
||||
description: 'A comprehensive package with premium inclusions and expanded service options.',
|
||||
},
|
||||
{
|
||||
id: 'catholic',
|
||||
name: 'Catholic Service',
|
||||
price: 3200,
|
||||
description:
|
||||
'Tailored for Catholic funeral traditions including a Requiem Mass and graveside prayers.',
|
||||
},
|
||||
];
|
||||
|
||||
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
|
||||
{
|
||||
id: 'rankins-standard',
|
||||
packageName: 'Standard Cremation Package',
|
||||
price: 2450,
|
||||
providerName: 'Rankins Funerals',
|
||||
location: 'Warrawong, NSW',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
},
|
||||
{
|
||||
id: 'easy-essential',
|
||||
packageName: 'Essential Funeral Service',
|
||||
price: 1950,
|
||||
providerName: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
},
|
||||
{
|
||||
id: 'killick-classic',
|
||||
packageName: 'Classic Farewell Package',
|
||||
price: 3100,
|
||||
providerName: 'Killick Family Funerals',
|
||||
location: 'Shellharbour, NSW',
|
||||
rating: 4.9,
|
||||
reviewCount: 15,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof UnverifiedPackageT2> = {
|
||||
title: 'Pages/UnverifiedPackageT2',
|
||||
component: UnverifiedPackageT2,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof UnverifiedPackageT2>;
|
||||
|
||||
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||
|
||||
/** Select a package to see the "Itemised Pricing Unavailable" detail panel */
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT2
|
||||
provider={mockProvider}
|
||||
packages={mockPackages}
|
||||
nearbyPackages={nearbyVerifiedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Make an enquiry')}
|
||||
onNearbyPackageClick={(id) => alert(`View nearby package: ${id}`)}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── With selection ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Package selected — detail panel shows price + unavailable notice */
|
||||
export const WithSelection: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>('everyday');
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT2
|
||||
provider={mockProvider}
|
||||
packages={mockPackages}
|
||||
nearbyPackages={nearbyVerifiedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Make an enquiry')}
|
||||
onNearbyPackageClick={(id) => alert(`View nearby package: ${id}`)}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── No nearby packages ────────────────────────────────────────────────────
|
||||
|
||||
/** Only this provider's packages — no nearby verified section */
|
||||
export const NoNearbyPackages: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT2
|
||||
provider={mockProvider}
|
||||
packages={mockPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Make an enquiry')}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Validation error ───────────────────────────────────────────────────────
|
||||
|
||||
/** Error shown when no package selected */
|
||||
export const WithError: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT2
|
||||
provider={mockProvider}
|
||||
packages={mockPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => {}}
|
||||
onBack={() => alert('Back')}
|
||||
error="Please choose a package to continue."
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
318
src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx
Normal file
318
src/components/pages/UnverifiedPackageT2/UnverifiedPackageT2.tsx
Normal file
@@ -0,0 +1,318 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
|
||||
import { ServiceOption } from '../../molecules/ServiceOption';
|
||||
import { PackageDetail } from '../../organisms/PackageDetail';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Provider summary for the compact card */
|
||||
export interface UnverifiedPackageT2Provider {
|
||||
/** Provider name */
|
||||
name: string;
|
||||
/** Location */
|
||||
location: string;
|
||||
/** Image URL */
|
||||
imageUrl?: string;
|
||||
/** Rating */
|
||||
rating?: number;
|
||||
/** Review count */
|
||||
reviewCount?: number;
|
||||
}
|
||||
|
||||
/** Package data — price only, no itemised breakdown */
|
||||
export interface UnverifiedPackageT2Data {
|
||||
/** Unique package ID */
|
||||
id: string;
|
||||
/** Package display name */
|
||||
name: string;
|
||||
/** Package price in dollars */
|
||||
price: number;
|
||||
/** Short description */
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/** A similar package from a nearby verified provider */
|
||||
export interface NearbyVerifiedPackage {
|
||||
/** Unique ID */
|
||||
id: string;
|
||||
/** Package name */
|
||||
packageName: string;
|
||||
/** Package price in dollars */
|
||||
price: number;
|
||||
/** Provider name */
|
||||
providerName: string;
|
||||
/** Provider location */
|
||||
location: string;
|
||||
/** Provider rating */
|
||||
rating?: number;
|
||||
/** Number of reviews */
|
||||
reviewCount?: number;
|
||||
}
|
||||
|
||||
/** Props for the UnverifiedPackageT2 page component */
|
||||
export interface UnverifiedPackageT2Props {
|
||||
/** Provider summary shown at top of the list panel (no image — unverified provider) */
|
||||
provider: UnverifiedPackageT2Provider;
|
||||
/** Packages with price only (no itemised breakdown) */
|
||||
packages: UnverifiedPackageT2Data[];
|
||||
/** Similar packages from nearby verified providers */
|
||||
nearbyPackages?: NearbyVerifiedPackage[];
|
||||
/** Currently selected package ID */
|
||||
selectedPackageId: string | null;
|
||||
/** Callback when a package is selected */
|
||||
onSelectPackage: (id: string) => void;
|
||||
/** Callback when "Make an enquiry" is clicked */
|
||||
onArrange: () => void;
|
||||
/** Callback when a nearby verified package is clicked */
|
||||
onNearbyPackageClick?: (id: string) => void;
|
||||
/** Callback when the provider card is clicked */
|
||||
onProviderClick?: () => void;
|
||||
/** Callback for the Back button */
|
||||
onBack: () => void;
|
||||
/** Validation error */
|
||||
error?: string;
|
||||
/** Whether the enquiry action is loading */
|
||||
loading?: boolean;
|
||||
/** Navigation bar */
|
||||
navigation?: React.ReactNode;
|
||||
/** Whether this is a pre-planning flow */
|
||||
isPrePlanning?: boolean;
|
||||
/** MUI sx prop */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* UnverifiedPackageT2 — Package selection page for Tier 2 unverified providers.
|
||||
*
|
||||
* Similar to T3 but the provider has only shared overall package prices,
|
||||
* not itemised breakdowns. The detail panel shows an "Itemized Pricing
|
||||
* Unavailable" notice instead of line items.
|
||||
*
|
||||
* Two sections:
|
||||
* - **This provider's packages**: price-only, no breakdown available
|
||||
* - **Similar packages from verified providers nearby**: promoted alternatives
|
||||
*
|
||||
* Pure presentation component — props in, callbacks out.
|
||||
*/
|
||||
export const UnverifiedPackageT2: React.FC<UnverifiedPackageT2Props> = ({
|
||||
provider,
|
||||
packages,
|
||||
nearbyPackages = [],
|
||||
selectedPackageId,
|
||||
onSelectPackage,
|
||||
onArrange,
|
||||
onNearbyPackageClick,
|
||||
onProviderClick,
|
||||
onBack,
|
||||
error,
|
||||
loading = false,
|
||||
navigation,
|
||||
isPrePlanning = false,
|
||||
sx,
|
||||
}) => {
|
||||
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
|
||||
const hasNearbyPackages = nearbyPackages.length > 0;
|
||||
|
||||
const subheading = isPrePlanning
|
||||
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
|
||||
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.';
|
||||
|
||||
return (
|
||||
<WizardLayout
|
||||
variant="list-detail"
|
||||
navigation={navigation}
|
||||
showBackLink
|
||||
backLabel="Back"
|
||||
onBack={onBack}
|
||||
sx={sx}
|
||||
secondaryPanel={
|
||||
selectedPackage ? (
|
||||
<PackageDetail
|
||||
name={selectedPackage.name}
|
||||
price={selectedPackage.price}
|
||||
sections={[]}
|
||||
onArrange={onArrange}
|
||||
arrangeDisabled={loading}
|
||||
arrangeLabel="Make an enquiry"
|
||||
priceDisclaimer="Prices are estimates based on publicly available information and may not reflect the provider's current pricing."
|
||||
itemizedUnavailable
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
minHeight: 300,
|
||||
bgcolor: 'var(--fa-color-brand-50)',
|
||||
borderRadius: 2,
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
Select a package to see more details.
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Provider compact card — no image for unverified */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ProviderCardCompact
|
||||
name={provider.name}
|
||||
location={provider.location}
|
||||
rating={provider.rating}
|
||||
reviewCount={provider.reviewCount}
|
||||
onClick={onProviderClick}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Heading */}
|
||||
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Explore available packages
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subheading}
|
||||
</Typography>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* ─── Packages ─── */}
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-label="Funeral packages"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
|
||||
>
|
||||
{packages.map((pkg) => (
|
||||
<ServiceOption
|
||||
key={pkg.id}
|
||||
name={pkg.name}
|
||||
description={pkg.description}
|
||||
price={pkg.price}
|
||||
selected={selectedPackageId === pkg.id}
|
||||
onClick={() => onSelectPackage(pkg.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{packages.length === 0 && (
|
||||
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No packages match your current preferences.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ─── Similar packages from nearby verified providers ─── */}
|
||||
{hasNearbyPackages && (
|
||||
<>
|
||||
<Divider sx={{ mb: 2.5 }} />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||
Similar packages from verified providers nearby
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
aria-label="Similar packages from nearby verified providers"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
|
||||
>
|
||||
{nearbyPackages.map((pkg) => (
|
||||
<Card
|
||||
key={pkg.id}
|
||||
variant="outlined"
|
||||
interactive={!!onNearbyPackageClick}
|
||||
padding="none"
|
||||
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
|
||||
sx={{ p: 'var(--fa-card-padding-compact)' }}
|
||||
>
|
||||
{/* Package name + price */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="span">
|
||||
{pkg.packageName}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="labelLg"
|
||||
component="span"
|
||||
color="primary"
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
${pkg.price.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Provider info */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pkg.providerName}
|
||||
</Typography>
|
||||
{pkg.rating != null && (
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
·
|
||||
</Typography>
|
||||
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.rating}
|
||||
{pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
·
|
||||
</Typography>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{ fontSize: 14, color: 'text.secondary' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</WizardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
UnverifiedPackageT2.displayName = 'UnverifiedPackageT2';
|
||||
export default UnverifiedPackageT2;
|
||||
2
src/components/pages/UnverifiedPackageT2/index.ts
Normal file
2
src/components/pages/UnverifiedPackageT2/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './UnverifiedPackageT2';
|
||||
export * from './UnverifiedPackageT2';
|
||||
@@ -0,0 +1,249 @@
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { UnverifiedPackageT3 } from './UnverifiedPackageT3';
|
||||
import type {
|
||||
UnverifiedPackageT3Data,
|
||||
UnverifiedPackageT3Provider,
|
||||
NearbyVerifiedPackage,
|
||||
} from './UnverifiedPackageT3';
|
||||
import { Navigation } from '../../organisms/Navigation';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const FALogo = () => (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-full.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'none', md: 'block' } }}
|
||||
/>
|
||||
<Box
|
||||
component="img"
|
||||
src="/brandlogo/logo-short.svg"
|
||||
alt="Funeral Arranger"
|
||||
sx={{ height: 28, display: { xs: 'block', md: 'none' } }}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
|
||||
const nav = (
|
||||
<Navigation
|
||||
logo={<FALogo />}
|
||||
items={[
|
||||
{ label: 'FAQ', href: '/faq' },
|
||||
{ label: 'Contact Us', href: '/contact' },
|
||||
{ label: 'Log in', href: '/login' },
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
const mockProvider: UnverifiedPackageT3Provider = {
|
||||
name: 'H.Parsons Funeral Directors',
|
||||
location: 'Wentworth, NSW',
|
||||
rating: 4.6,
|
||||
reviewCount: 7,
|
||||
};
|
||||
|
||||
const matchedPackages: UnverifiedPackageT3Data[] = [
|
||||
{
|
||||
id: 'everyday',
|
||||
name: 'Everyday Funeral Package',
|
||||
price: 2700,
|
||||
description:
|
||||
'This package includes a funeral service at a chapel or a church with a funeral procession. It includes many of the most commonly selected funeral options.',
|
||||
sections: [
|
||||
{
|
||||
heading: 'Essentials',
|
||||
items: [
|
||||
{ name: 'Accommodation', price: 500 },
|
||||
{ name: 'Death registration certificate', price: 150 },
|
||||
{ name: 'Doctor fee for Cremation', price: 150 },
|
||||
{ name: 'NSW Government Levy - Cremation', price: 83 },
|
||||
{ name: 'Professional Mortuary Care', price: 1200 },
|
||||
{ name: 'Professional Service Fee', price: 1120 },
|
||||
],
|
||||
},
|
||||
{
|
||||
heading: 'Complimentary Items',
|
||||
items: [
|
||||
{ name: 'Dressing Fee', price: 0 },
|
||||
{ name: 'Viewing Fee', price: 0 },
|
||||
],
|
||||
},
|
||||
],
|
||||
total: 2700,
|
||||
extras: {
|
||||
heading: 'Extras',
|
||||
items: [
|
||||
{ name: 'Allowance for Flowers', price: 150, isAllowance: true },
|
||||
{ name: 'Allowance for Master of Ceremonies', price: 500, isAllowance: true },
|
||||
{ name: 'After Business Hours Service Surcharge', price: 150 },
|
||||
{ name: 'After Hours Prayers', price: 1920 },
|
||||
{ name: 'Coffin Bearing by Funeral Directors', price: 1500 },
|
||||
{ name: 'Digital Recording', price: 500 },
|
||||
],
|
||||
},
|
||||
terms:
|
||||
'This package includes a funeral service at a chapel or a church with a funeral procession. Pricing may vary based on additional selections.',
|
||||
},
|
||||
];
|
||||
|
||||
const nearbyVerifiedPackages: NearbyVerifiedPackage[] = [
|
||||
{
|
||||
id: 'rankins-standard',
|
||||
packageName: 'Standard Cremation Package',
|
||||
price: 2450,
|
||||
providerName: 'Rankins Funerals',
|
||||
location: 'Warrawong, NSW',
|
||||
rating: 4.8,
|
||||
reviewCount: 23,
|
||||
},
|
||||
{
|
||||
id: 'easy-essential',
|
||||
packageName: 'Essential Funeral Service',
|
||||
price: 1950,
|
||||
providerName: 'Easy Funerals',
|
||||
location: 'Sydney, NSW',
|
||||
rating: 4.5,
|
||||
reviewCount: 42,
|
||||
},
|
||||
{
|
||||
id: 'killick-classic',
|
||||
packageName: 'Classic Farewell Package',
|
||||
price: 3100,
|
||||
providerName: 'Killick Family Funerals',
|
||||
location: 'Shellharbour, NSW',
|
||||
rating: 4.9,
|
||||
reviewCount: 15,
|
||||
},
|
||||
];
|
||||
|
||||
// ─── Meta ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const meta: Meta<typeof UnverifiedPackageT3> = {
|
||||
title: 'Pages/UnverifiedPackageT3',
|
||||
component: UnverifiedPackageT3,
|
||||
tags: ['autodocs'],
|
||||
parameters: {
|
||||
layout: 'fullscreen',
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof UnverifiedPackageT3>;
|
||||
|
||||
// ─── Interactive (default) ──────────────────────────────────────────────────
|
||||
|
||||
/** Matched + other packages — select a package, see detail, click Make Arrangement */
|
||||
export const Default: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT3
|
||||
provider={mockProvider}
|
||||
packages={matchedPackages}
|
||||
nearbyPackages={nearbyVerifiedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Open ArrangementDialog')}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── With selection ─────────────────────────────────────────────────────────
|
||||
|
||||
/** Package already selected — detail panel visible */
|
||||
export const WithSelection: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>('everyday');
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT3
|
||||
provider={mockProvider}
|
||||
packages={matchedPackages}
|
||||
nearbyPackages={nearbyVerifiedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Open ArrangementDialog')}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── No other packages (all match) ─────────────────────────────────────────
|
||||
|
||||
/** No nearby verified packages — only this provider's packages */
|
||||
export const NoNearbyPackages: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT3
|
||||
provider={mockProvider}
|
||||
packages={matchedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Open ArrangementDialog')}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Pre-planning ───────────────────────────────────────────────────────────
|
||||
|
||||
/** Pre-planning flow — softer copy */
|
||||
export const PrePlanning: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT3
|
||||
provider={mockProvider}
|
||||
packages={matchedPackages}
|
||||
nearbyPackages={nearbyVerifiedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => alert('Open ArrangementDialog')}
|
||||
onProviderClick={() => alert('Open provider profile')}
|
||||
onBack={() => alert('Back')}
|
||||
navigation={nav}
|
||||
isPrePlanning
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
// ─── Validation error ───────────────────────────────────────────────────────
|
||||
|
||||
/** Error shown when no package selected */
|
||||
export const WithError: Story = {
|
||||
render: () => {
|
||||
const [selectedId, setSelectedId] = useState<string | null>(null);
|
||||
|
||||
return (
|
||||
<UnverifiedPackageT3
|
||||
provider={mockProvider}
|
||||
packages={matchedPackages}
|
||||
selectedPackageId={selectedId}
|
||||
onSelectPackage={setSelectedId}
|
||||
onArrange={() => {}}
|
||||
onBack={() => alert('Back')}
|
||||
error="Please choose a package to continue."
|
||||
navigation={nav}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
333
src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
Normal file
333
src/components/pages/UnverifiedPackageT3/UnverifiedPackageT3.tsx
Normal file
@@ -0,0 +1,333 @@
|
||||
import React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import LocationOnOutlinedIcon from '@mui/icons-material/LocationOnOutlined';
|
||||
import StarRoundedIcon from '@mui/icons-material/StarRounded';
|
||||
import VerifiedOutlinedIcon from '@mui/icons-material/VerifiedOutlined';
|
||||
import type { SxProps, Theme } from '@mui/material/styles';
|
||||
import { WizardLayout } from '../../templates/WizardLayout';
|
||||
import { ProviderCardCompact } from '../../molecules/ProviderCardCompact';
|
||||
import { ServiceOption } from '../../molecules/ServiceOption';
|
||||
import { PackageDetail } from '../../organisms/PackageDetail';
|
||||
import type { PackageSection } from '../../organisms/PackageDetail';
|
||||
import { Typography } from '../../atoms/Typography';
|
||||
import { Card } from '../../atoms/Card';
|
||||
import { Divider } from '../../atoms/Divider';
|
||||
|
||||
// ─── Types ───────────────────────────────────────────────────────────────────
|
||||
|
||||
/** Provider summary for the compact card */
|
||||
export interface UnverifiedPackageT3Provider {
|
||||
/** Provider name */
|
||||
name: string;
|
||||
/** Location */
|
||||
location: string;
|
||||
/** Image URL */
|
||||
imageUrl?: string;
|
||||
/** Rating */
|
||||
rating?: number;
|
||||
/** Review count */
|
||||
reviewCount?: number;
|
||||
}
|
||||
|
||||
/** Package data for the selection list */
|
||||
export interface UnverifiedPackageT3Data {
|
||||
/** Unique package ID */
|
||||
id: string;
|
||||
/** Package display name */
|
||||
name: string;
|
||||
/** Package price in dollars */
|
||||
price: number;
|
||||
/** Short description */
|
||||
description?: string;
|
||||
/** Line item sections for the detail panel */
|
||||
sections: PackageSection[];
|
||||
/** Total price (may differ from base price with extras) */
|
||||
total?: number;
|
||||
/** Extra items section (after total) */
|
||||
extras?: PackageSection;
|
||||
/** Terms and conditions */
|
||||
terms?: string;
|
||||
}
|
||||
|
||||
/** A similar package from a nearby verified provider */
|
||||
export interface NearbyVerifiedPackage {
|
||||
/** Unique ID */
|
||||
id: string;
|
||||
/** Package name */
|
||||
packageName: string;
|
||||
/** Package price in dollars */
|
||||
price: number;
|
||||
/** Provider name */
|
||||
providerName: string;
|
||||
/** Provider location */
|
||||
location: string;
|
||||
/** Provider rating */
|
||||
rating?: number;
|
||||
/** Number of reviews */
|
||||
reviewCount?: number;
|
||||
}
|
||||
|
||||
/** Props for the UnverifiedPackageT3 page component */
|
||||
export interface UnverifiedPackageT3Props {
|
||||
/** Provider summary shown at top of the list panel (no image — unverified provider) */
|
||||
provider: UnverifiedPackageT3Provider;
|
||||
/** Packages matching the user's filters from the previous step */
|
||||
packages: UnverifiedPackageT3Data[];
|
||||
/** Similar packages from nearby verified providers */
|
||||
nearbyPackages?: NearbyVerifiedPackage[];
|
||||
/** Currently selected package ID */
|
||||
selectedPackageId: string | null;
|
||||
/** Callback when a package is selected */
|
||||
onSelectPackage: (id: string) => void;
|
||||
/** Callback when "Make Arrangement" is clicked (opens ArrangementDialog) */
|
||||
onArrange: () => void;
|
||||
/** Callback when a nearby verified package is clicked */
|
||||
onNearbyPackageClick?: (id: string) => void;
|
||||
/** Callback when the provider card is clicked (opens provider profile popup) */
|
||||
onProviderClick?: () => void;
|
||||
/** Callback for the Back button */
|
||||
onBack: () => void;
|
||||
/** Validation error */
|
||||
error?: string;
|
||||
/** Whether the arrange action is loading */
|
||||
loading?: boolean;
|
||||
/** Navigation bar */
|
||||
navigation?: React.ReactNode;
|
||||
/** Whether this is a pre-planning flow */
|
||||
isPrePlanning?: boolean;
|
||||
/** MUI sx prop */
|
||||
sx?: SxProps<Theme>;
|
||||
}
|
||||
|
||||
// ─── Component ───────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* UnverifiedPackageT3 — Package selection page for unverified (Tier 3) providers.
|
||||
*
|
||||
* List + Detail split layout. Left panel shows the selected provider
|
||||
* (compact) and selectable package cards. Right panel shows the full
|
||||
* detail breakdown of the selected package with "Make Arrangement" CTA.
|
||||
*
|
||||
* Two sections:
|
||||
* - **This provider's packages**: estimated pricing from publicly available info
|
||||
* - **Similar packages from verified providers nearby**: promoted alternatives
|
||||
* with verified pricing, ratings, and location
|
||||
*
|
||||
* Selecting a package reveals its detail. Clicking "Make an enquiry"
|
||||
* on the detail panel initiates contact with the unverified provider.
|
||||
*
|
||||
* Pure presentation component — props in, callbacks out.
|
||||
*/
|
||||
export const UnverifiedPackageT3: React.FC<UnverifiedPackageT3Props> = ({
|
||||
provider,
|
||||
packages,
|
||||
nearbyPackages = [],
|
||||
selectedPackageId,
|
||||
onSelectPackage,
|
||||
onArrange,
|
||||
onNearbyPackageClick,
|
||||
onProviderClick,
|
||||
onBack,
|
||||
error,
|
||||
loading = false,
|
||||
navigation,
|
||||
isPrePlanning = false,
|
||||
sx,
|
||||
}) => {
|
||||
const selectedPackage = packages.find((p) => p.id === selectedPackageId);
|
||||
const hasNearbyPackages = nearbyPackages.length > 0;
|
||||
|
||||
const subheading = isPrePlanning
|
||||
? 'Browse estimated packages to get a sense of what this provider offers. Nothing is committed.'
|
||||
: 'These packages are based on publicly available information. Make an enquiry to confirm details and pricing.';
|
||||
|
||||
return (
|
||||
<WizardLayout
|
||||
variant="list-detail"
|
||||
navigation={navigation}
|
||||
showBackLink
|
||||
backLabel="Back"
|
||||
onBack={onBack}
|
||||
sx={sx}
|
||||
secondaryPanel={
|
||||
selectedPackage ? (
|
||||
<PackageDetail
|
||||
name={selectedPackage.name}
|
||||
price={selectedPackage.price}
|
||||
sections={selectedPackage.sections}
|
||||
total={selectedPackage.total}
|
||||
extras={selectedPackage.extras}
|
||||
terms={selectedPackage.terms}
|
||||
onArrange={onArrange}
|
||||
arrangeDisabled={loading}
|
||||
arrangeLabel="Make an enquiry"
|
||||
priceDisclaimer="Prices are estimates based on publicly available information and may not reflect the provider's current pricing."
|
||||
/>
|
||||
) : (
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
height: '100%',
|
||||
minHeight: 300,
|
||||
bgcolor: 'var(--fa-color-brand-50)',
|
||||
borderRadius: 2,
|
||||
p: 4,
|
||||
}}
|
||||
>
|
||||
<Typography variant="body1" color="text.secondary" sx={{ textAlign: 'center' }}>
|
||||
Select a package to see what's included.
|
||||
</Typography>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
>
|
||||
{/* Provider compact card — clickable to open provider profile */}
|
||||
<Box sx={{ mb: 3 }}>
|
||||
<ProviderCardCompact
|
||||
name={provider.name}
|
||||
location={provider.location}
|
||||
rating={provider.rating}
|
||||
reviewCount={provider.reviewCount}
|
||||
onClick={onProviderClick}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Heading */}
|
||||
<Typography variant="h4" component="h1" sx={{ mb: 0.5 }} tabIndex={-1}>
|
||||
Explore available packages
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mb: 3 }}>
|
||||
{subheading}
|
||||
</Typography>
|
||||
|
||||
{/* Error message */}
|
||||
{error && (
|
||||
<Typography
|
||||
variant="body2"
|
||||
sx={{ mb: 2, color: 'var(--fa-color-text-brand)' }}
|
||||
role="alert"
|
||||
>
|
||||
{error}
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
{/* ─── Packages ─── */}
|
||||
<Box
|
||||
role="radiogroup"
|
||||
aria-label="Funeral packages"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
|
||||
>
|
||||
{packages.map((pkg) => (
|
||||
<ServiceOption
|
||||
key={pkg.id}
|
||||
name={pkg.name}
|
||||
description={pkg.description}
|
||||
price={pkg.price}
|
||||
selected={selectedPackageId === pkg.id}
|
||||
onClick={() => onSelectPackage(pkg.id)}
|
||||
/>
|
||||
))}
|
||||
|
||||
{packages.length === 0 && (
|
||||
<Box sx={{ py: 4, textAlign: 'center' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
No packages match your current preferences.
|
||||
</Typography>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{/* ─── Similar packages from nearby verified providers ─── */}
|
||||
{hasNearbyPackages && (
|
||||
<>
|
||||
<Divider sx={{ mb: 2.5 }} />
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 1,
|
||||
mb: 2,
|
||||
}}
|
||||
>
|
||||
<VerifiedOutlinedIcon sx={{ fontSize: 16, color: 'primary.main' }} aria-hidden />
|
||||
<Typography variant="body2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
||||
Similar packages from verified providers nearby
|
||||
</Typography>
|
||||
</Box>
|
||||
<Box
|
||||
aria-label="Similar packages from nearby verified providers"
|
||||
sx={{ display: 'flex', flexDirection: 'column', gap: 2, mb: 3 }}
|
||||
>
|
||||
{nearbyPackages.map((pkg) => (
|
||||
<Card
|
||||
key={pkg.id}
|
||||
variant="outlined"
|
||||
interactive={!!onNearbyPackageClick}
|
||||
padding="none"
|
||||
onClick={onNearbyPackageClick ? () => onNearbyPackageClick(pkg.id) : undefined}
|
||||
sx={{ p: 'var(--fa-card-padding-compact)' }}
|
||||
>
|
||||
{/* Package name + price */}
|
||||
<Box
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'flex-start',
|
||||
justifyContent: 'space-between',
|
||||
gap: 2,
|
||||
mb: 1,
|
||||
}}
|
||||
>
|
||||
<Typography variant="h6" component="span">
|
||||
{pkg.packageName}
|
||||
</Typography>
|
||||
<Typography
|
||||
variant="labelLg"
|
||||
component="span"
|
||||
color="primary"
|
||||
sx={{ whiteSpace: 'nowrap' }}
|
||||
>
|
||||
${pkg.price.toLocaleString('en-AU')}
|
||||
</Typography>
|
||||
</Box>
|
||||
|
||||
{/* Provider info */}
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 0.5, flexWrap: 'wrap' }}>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{pkg.providerName}
|
||||
</Typography>
|
||||
{pkg.rating != null && (
|
||||
<>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
·
|
||||
</Typography>
|
||||
<StarRoundedIcon sx={{ fontSize: 14, color: 'warning.main' }} aria-hidden />
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.rating}
|
||||
{pkg.reviewCount != null ? ` (${pkg.reviewCount})` : ''}
|
||||
</Typography>
|
||||
</>
|
||||
)}
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
·
|
||||
</Typography>
|
||||
<LocationOnOutlinedIcon
|
||||
sx={{ fontSize: 14, color: 'text.secondary' }}
|
||||
aria-hidden
|
||||
/>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
{pkg.location}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Card>
|
||||
))}
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</WizardLayout>
|
||||
);
|
||||
};
|
||||
|
||||
UnverifiedPackageT3.displayName = 'UnverifiedPackageT3';
|
||||
export default UnverifiedPackageT3;
|
||||
2
src/components/pages/UnverifiedPackageT3/index.ts
Normal file
2
src/components/pages/UnverifiedPackageT3/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { default } from './UnverifiedPackageT3';
|
||||
export * from './UnverifiedPackageT3';
|
||||
Reference in New Issue
Block a user