From 8b729d7fc469f25ba0b39238c7786f4d47dfe77a Mon Sep 17 00:00:00 2001 From: Paul Huliganga Date: Sun, 29 Mar 2026 23:11:27 -0400 Subject: [PATCH] chore(backend): implement high-priority improvements from code review - Configuration: dotenv support, env vars for PORT, DB_PATH, CORS_ORIGIN, rate limits - Security: API key auth for write endpoints, rate limiting on import, configurable CORS, image URL normalization - Reliability: DB transactions for create/update, dirty flag for saves, foreign key enforcement (PRAGMA), duplicate detection O(1) optimization - Features: health check endpoint (/api/health) - Bugfix: corrected tag assignment/removal routes (param order) - Testing: added tests for PUT/DELETE recipes, tag CRUD and assignment, enabled foreign keys in tests - All 46 backend tests passing Closes # --- .env.example | 20 + .harness/docs/styling-governance.md | 70 ++ .harness/docs/styling-inventory.md | 184 +++++ .harness/docs/styling-stabilization-qa.md | 132 ++++ .harness/docs/styling-token-contract.md | 50 ++ .../docs/t05-legacy-style-debt-cleanup.md | 40 + .harness/docs/ui-primitive-contract.md | 91 +++ .../image-import-hardening-execution-board.md | 234 ++++++ .harness/local-file-import-execution-board.md | 190 +++++ .harness/local-file-import-phase1-analysis.md | 243 ++++++ .../styling-stabilization-execution-board.md | 225 ++++++ .harness/tasks-visual-refresh.md | 70 ++ .harness/tasks-workflow-stabilization.md | 73 ++ README.md | 5 + ROADMAP.md | 2 + TODO.md | 41 ++ audit-fix-images.js | 75 ++ docs/planning-terminology.md | 16 + docs/recipe-manager-mvp-plan.md | 53 ++ .../after/homepage-qa-note-t04.md | 25 + docs/visual-audit/assets.md | 88 +-- docs/visual-audit/before/README.md | 28 + .../before/acceptance-checklist.md | 27 + .../before/ui-ux-baseline-audit.md | 87 +++ docs/visual-audit/photo-first-assets.md | 87 +++ docs/visual-style-guide.md | 95 +++ fix-image-paths.js | 43 ++ frontend/README.md | 14 + frontend/package.json | 4 +- frontend/public/assets/README.md | 35 +- .../illustrations/category-breakfast.svg | 1 + .../illustrations/category-dessert.svg | 1 + .../illustrations/category-dinner.svg | 1 + .../assets/empty-state/no-cook-history.svg | 1 + .../public/assets/food/curated/bowl-salad.svg | 1 + .../assets/food/curated/breakfast-toast.svg | 1 + .../assets/food/curated/dessert-cake.svg | 1 + .../assets/food/curated/pasta-plate.svg | 1 + .../assets/food/curated/smoothie-glass.svg | 1 + .../public/assets/food/curated/soup-bowl.svg | 1 + .../public/assets/hero/hero-fresh-produce.svg | 25 + .../public/assets/hero/hero-kitchen-light.svg | 28 + frontend/public/assets/photos/README.md | 34 + .../public/assets/photos/category/.gitkeep | 0 .../public/assets/photos/credits/.gitkeep | 0 frontend/public/assets/photos/detail/.gitkeep | 0 .../public/assets/photos/empty-state/.gitkeep | 0 frontend/public/assets/photos/hero/.gitkeep | 0 frontend/public/assets/photos/list/.gitkeep | 0 frontend/public/assets/photos/manifest.json | 76 ++ frontend/scripts/style-guardrails.mjs | 76 ++ frontend/src/App.css | 184 ----- frontend/src/App.tsx | 54 +- frontend/src/assets/photoManifest.ts | 42 ++ frontend/src/assets/visualAssets.ts | 14 +- frontend/src/components/ErrorBoundary.tsx | 34 +- .../src/components/MissionControlPanel.tsx | 32 +- frontend/src/components/RecipeCard.tsx | 315 ++++++-- frontend/src/components/RecipeForm.tsx | 54 +- frontend/src/components/TagSelector.tsx | 22 +- frontend/src/components/Toast.tsx | 2 +- frontend/src/components/ui/cn.ts | 5 + frontend/src/components/ui/primitives.tsx | 98 +++ frontend/src/hooks/useRecipes.ts | 11 +- frontend/src/index.css | 249 ++++--- frontend/src/pages/CookModePage.tsx | 202 +++-- frontend/src/pages/ImportUrlPage.tsx | 204 ++++-- frontend/src/pages/NotFoundPage.tsx | 20 +- frontend/src/pages/RecipeDetailPage.tsx | 601 +++++++++++---- frontend/src/pages/RecipeListPage.tsx | 438 ++++++++--- frontend/src/services/api.ts | 131 +++- frontend/src/styles/tokens.css | 111 +++ frontend/src/theme.ts | 168 +++-- frontend/src/types/recipe.ts | 9 +- frontend/tailwind.config.js | 2 +- frontend/vite.config.ts | 5 + import_report.md | 11 + migrations/2026-03-27-add-recipe-image-url.md | 26 + .../2026-03-28-add-user-metadata-fields.md | 43 ++ package-lock.json | 693 +++++++++++++++++- package.json | 8 +- recipes_store.jsonl | 0 scripts/import_copymethat.py | 86 +++ scripts/test-import.sh | 118 +++ scripts/test-parser-direct.cjs | 47 ++ src/backend/db/database.ts | 16 + src/backend/db/migrate.ts | 19 +- src/backend/db/schema.sql | 6 +- src/backend/db/schemaMigrations.ts | 11 + src/backend/index.ts | 93 ++- src/backend/repositories/RecipeRepository.ts | 191 +++-- src/backend/routes/import.ts | 289 +++++++- src/backend/routes/importLocal.ts | 152 ++++ src/backend/routes/recipes.ts | 33 +- src/backend/routes/tags.ts | 16 +- src/backend/services/CopyMeThatHtmlParser.ts | 248 +++++++ .../services/CopyMeThatImportService.ts | 154 ++++ src/backend/services/CopyMeThatTxtParser.ts | 140 ++++ .../services/HeuristicRecipeParserService.ts | 3 +- .../services/SchemaOrgRecipeParserService.ts | 1 + src/backend/tests/import.test.ts | 96 ++- src/backend/tests/recipes.test.ts | 93 +++ src/backend/tests/tags.test.ts | 124 ++++ src/backend/types/recipe.ts | 13 + status/phase-progress.jsonl | 4 +- status/phase-updates.jsonl | 35 +- status/test-workflow-status-orchestrator.json | 2 +- status/test-workflow-status-status.json | 2 +- status/workflow-status.json | 8 +- update-db-images.js | 38 + 110 files changed, 7268 insertions(+), 1154 deletions(-) create mode 100644 .env.example create mode 100644 .harness/docs/styling-governance.md create mode 100644 .harness/docs/styling-inventory.md create mode 100644 .harness/docs/styling-stabilization-qa.md create mode 100644 .harness/docs/styling-token-contract.md create mode 100644 .harness/docs/t05-legacy-style-debt-cleanup.md create mode 100644 .harness/docs/ui-primitive-contract.md create mode 100644 .harness/image-import-hardening-execution-board.md create mode 100644 .harness/local-file-import-execution-board.md create mode 100644 .harness/local-file-import-phase1-analysis.md create mode 100644 .harness/styling-stabilization-execution-board.md create mode 100644 .harness/tasks-visual-refresh.md create mode 100644 .harness/tasks-workflow-stabilization.md create mode 100644 audit-fix-images.js create mode 100644 docs/planning-terminology.md create mode 100644 docs/visual-audit/after/homepage-qa-note-t04.md create mode 100644 docs/visual-audit/before/README.md create mode 100644 docs/visual-audit/before/acceptance-checklist.md create mode 100644 docs/visual-audit/before/ui-ux-baseline-audit.md create mode 100644 docs/visual-audit/photo-first-assets.md create mode 100644 docs/visual-style-guide.md create mode 100644 fix-image-paths.js create mode 100644 frontend/public/assets/category/illustrations/category-breakfast.svg create mode 100644 frontend/public/assets/category/illustrations/category-dessert.svg create mode 100644 frontend/public/assets/category/illustrations/category-dinner.svg create mode 100644 frontend/public/assets/empty-state/no-cook-history.svg create mode 100644 frontend/public/assets/food/curated/bowl-salad.svg create mode 100644 frontend/public/assets/food/curated/breakfast-toast.svg create mode 100644 frontend/public/assets/food/curated/dessert-cake.svg create mode 100644 frontend/public/assets/food/curated/pasta-plate.svg create mode 100644 frontend/public/assets/food/curated/smoothie-glass.svg create mode 100644 frontend/public/assets/food/curated/soup-bowl.svg create mode 100644 frontend/public/assets/hero/hero-fresh-produce.svg create mode 100644 frontend/public/assets/hero/hero-kitchen-light.svg create mode 100644 frontend/public/assets/photos/README.md create mode 100644 frontend/public/assets/photos/category/.gitkeep create mode 100644 frontend/public/assets/photos/credits/.gitkeep create mode 100644 frontend/public/assets/photos/detail/.gitkeep create mode 100644 frontend/public/assets/photos/empty-state/.gitkeep create mode 100644 frontend/public/assets/photos/hero/.gitkeep create mode 100644 frontend/public/assets/photos/list/.gitkeep create mode 100644 frontend/public/assets/photos/manifest.json create mode 100644 frontend/scripts/style-guardrails.mjs delete mode 100644 frontend/src/App.css create mode 100644 frontend/src/assets/photoManifest.ts create mode 100644 frontend/src/components/ui/cn.ts create mode 100644 frontend/src/components/ui/primitives.tsx create mode 100644 frontend/src/styles/tokens.css create mode 100644 import_report.md create mode 100644 migrations/2026-03-27-add-recipe-image-url.md create mode 100644 migrations/2026-03-28-add-user-metadata-fields.md create mode 100644 recipes_store.jsonl create mode 100644 scripts/import_copymethat.py create mode 100755 scripts/test-import.sh create mode 100644 scripts/test-parser-direct.cjs create mode 100644 src/backend/routes/importLocal.ts create mode 100644 src/backend/services/CopyMeThatHtmlParser.ts create mode 100644 src/backend/services/CopyMeThatImportService.ts create mode 100644 src/backend/services/CopyMeThatTxtParser.ts create mode 100644 update-db-images.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1c32ca1 --- /dev/null +++ b/.env.example @@ -0,0 +1,20 @@ +# Server configuration +PORT=3000 +DATABASE_PATH=data/recipes.db +NODE_ENV=development + +# CORS configuration +# Set to specific origin in production (e.g., https://your-frontend.com) +# Use * for development only +ALLOWED_ORIGIN=* + +# Rate limiting (import endpoint) +IMPORT_RATE_LIMIT_WINDOW_MS=60000 +IMPORT_RATE_LIMIT_MAX_REQUESTS=10 + +# API authentication (optional but recommended for production) +# Set a strong random string if API_KEY is defined; write endpoints will require it +#API_KEY= + +# Logging +LOG_LEVEL=info \ No newline at end of file diff --git a/.harness/docs/styling-governance.md b/.harness/docs/styling-governance.md new file mode 100644 index 0000000..6a05f30 --- /dev/null +++ b/.harness/docs/styling-governance.md @@ -0,0 +1,70 @@ +# T06 — Styling Guardrails + Lightweight Governance + +Date: 2026-03-27 +Scope: `recipe-manager/frontend` + +## Why this exists + +The styling stabilization pass established a token + primitive contract. This governance doc keeps that contract from drifting back into mixed styling modes. + +## Canonical styling contract (short version) + +1. **Tokens are defined in one place:** `src/styles/tokens.css` +2. **Build UI from shared primitives first:** `src/components/ui/primitives.tsx` + `ui-*` classes from `src/index.css` +3. **Use tokenized values in classes/styles:** `var(--...)` or Tailwind theme aliases mapped to tokens +4. **Do not add new raw Tailwind palette utilities** (e.g. `bg-blue-500`, `text-slate-600`) in stabilized shared surfaces +5. **Do not hardcode hex colors in `className`** (e.g. `bg-[#1f2937]`) for app UI + +Reference docs: +- `./styling-token-contract.md` +- `./ui-primitive-contract.md` +- `./t05-legacy-style-debt-cleanup.md` + +## Lightweight automated guardrail + +A scoped check is available: + +```bash +cd frontend +npm run style:guardrails +``` + +Current guard scope (intentional, low-risk): +- `src/App.tsx` +- `src/components/MissionControlPanel.tsx` +- `src/components/ui/primitives.tsx` + +What it flags: +- direct palette utility classes (`bg-blue-*`, `text-slate-*`, etc.) +- hex colors in arbitrary utility classes (`bg-[#...]`, `text-[#...]`, etc.) + +If violations are found in guarded files, the script exits non-zero. + +## Contributor checklist (copy into PR template/comments as needed) + +- [ ] New UI uses `Ui*` primitives or existing `ui-*` classes where possible +- [ ] Any new visual token was added to `tokens.css` first +- [ ] No new hardcoded palette utility classes in stabilized shell/shared primitive areas +- [ ] No new hardcoded hex colors in class utilities for app surfaces +- [ ] `npm run style:guardrails` passes +- [ ] `npm run lint` passes + +## Review heuristics for agents/contributors + +When touching frontend UI, quickly answer: + +1. "Can this use an existing `Ui*` primitive?" +2. "If I need a new color/radius/shadow, did I add a semantic token first?" +3. "Am I introducing a second styling path that future edits will copy?" + +If the answer to (3) is yes, pause and refactor to the contract. + +## Intentional limitations (for now) + +- The guard script is **scoped**, not repo-wide, because existing pages still contain known legacy palette usage (tracked in T01/T04). +- The check is intentionally simple regex-based (fast, no lint plugin churn). +- This is not wired into heavy CI yet; it is run locally and can be added to future CI once high-drift pages are migrated. + +## Expansion path after T04/T07 + +As additional pages are stabilized, add them to `frontend/scripts/style-guardrails.mjs` scope to progressively tighten enforcement without breaking current workflows. diff --git a/.harness/docs/styling-inventory.md b/.harness/docs/styling-inventory.md new file mode 100644 index 0000000..c6eb7ca --- /dev/null +++ b/.harness/docs/styling-inventory.md @@ -0,0 +1,184 @@ +# T01 — Styling Inventory + Drift Map + +Date: 2026-03-27 +Project: `recipe-manager/frontend` +Task: **T01 — Styling Inventory + Drift Map** + +## Scope audited +- Token sources: `src/styles/tokens.css`, `src/theme.ts`, `frontend/tailwind.config.js` +- Shared styling layer: `src/index.css` (`ui-*` primitives) +- Legacy stylesheet: `src/App.css` +- App shell + representative screens/components: + - `src/App.tsx` + - `src/pages/RecipeListPage.tsx` + - `src/pages/RecipeDetailPage.tsx` + - `src/pages/CookModePage.tsx` + - `src/pages/ImportUrlPage.tsx` + - `src/pages/NotFoundPage.tsx` + - `src/components/RecipeCard.tsx` + - `src/components/RecipeForm.tsx` + - `src/components/TagSelector.tsx` + - `src/components/Toast.tsx` + - `src/components/MissionControlPanel.tsx` + - `src/components/ErrorBoundary.tsx` + +--- + +## 1) Where styling primitives currently live + +### Canonical CSS token surface (already strong) +- **`src/styles/tokens.css`** + - semantic color, typography, spacing, radius, shadow, focus, gradients + - includes dark-mode token overrides + +### Utility-class primitives (already present) +- **`src/index.css` @layer components** defines: + - `.ui-page`, `.ui-section`, `.ui-card` + - `.ui-btn`, `.ui-btn-primary`, `.ui-btn-secondary` + - `.ui-input`, `.ui-textarea`, `.ui-select` + - `.ui-chip`, `.ui-badge` +- Also contains global focus-visible behavior and utility legacy classes (`.card`, `.shadow-card`, `animate-slide-in`) + +### Tailwind token mapping +- **`frontend/tailwind.config.js`** maps many Tailwind theme keys to CSS vars (good bridge) + +### Parallel TS token layer (drift risk) +- **`src/theme.ts`** duplicates many token values from `tokens.css` + - `colors`, `typography`, `spacing`, `radius`, `shadows`, `componentStyles` + +### Legacy stylesheet +- **`src/App.css`** contains Vite/demo-era nested selectors (`#next-steps`, `.hero`, `.counter`, etc.) +- not imported by `main.tsx` or other src files in current app + +--- + +## 2) Systems currently being mixed + +1. **UI primitives + token vars** (`ui-*`, `var(--...)`) +2. **Raw Tailwind palette utilities** (`bg-blue-*`, `text-slate-*`, `border-gray-*`, etc.) +3. **Inline style objects via `theme.ts`** (`style={{ borderRadius: radius.lg }}`, `boxShadow: shadows.card`) +4. **One-off arbitrary values** (`hover:[box-shadow:var(--shadow-hover)]`, `shadow-[...]`) +5. **Legacy class debt** (`App.css`, and old helper classes in `index.css`) + +Result: there is no enforced single styling path; authors can pick multiple parallel approaches in the same file. + +--- + +## 3) Inventory by file (top-level pages + shared components) + +Legend: +- **A** = ui-* primitives + var tokens (preferred direction) +- **B** = raw Tailwind palette utilities (non-tokenized drift) +- **C** = inline `theme.ts` style objects +- **D** = legacy/older pattern + +### App shell +- `src/App.tsx` → **B-heavy** + - Header/nav/footer use many `blue/slate/gray` classes and custom focus ring colors + - Does not use `ui-page`, `ui-section`, `ui-btn` contract + +### Pages +- `src/pages/RecipeListPage.tsx` → **A + C mix (plus some B)** + - strong `ui-*` adoption + - frequent inline radius/style injections from `theme.ts` +- `src/pages/RecipeDetailPage.tsx` → **A + B + C mixed** + - uses many `ui-*` classes but also many non-token Tailwind palette classes and inline styles +- `src/pages/CookModePage.tsx` → **B-only (high drift)** + - almost entirely gray/blue/green/red Tailwind palette utilities + - no `ui-*` shell/primitives +- `src/pages/ImportUrlPage.tsx` → **B + C (high drift)** + - heavily uses slate/blue/indigo/green palette classes + - inline radius/progress width styles +- `src/pages/NotFoundPage.tsx` → **B-only** + +### Shared components +- `src/components/RecipeForm.tsx` → **A-dominant** +- `src/components/TagSelector.tsx` → **A-dominant + tiny C** +- `src/components/RecipeCard.tsx` → **A + C mixed** +- `src/components/Toast.tsx` → **C-dominant** + - visual appearance mostly from `theme.ts` inline styles +- `src/components/MissionControlPanel.tsx` → **B-only** +- `src/components/ErrorBoundary.tsx` → **B-only** + +### Approximate drift signal (quick scan) +- Highest raw palette concentration: `CookModePage`, `ImportUrlPage`, `App.tsx`, `RecipeDetailPage` +- Highest inline-style concentration: `RecipeListPage`, `RecipeCard`, `Toast` + +--- + +## 4) Representative drift hotspots + +1. **`App.tsx` (app shell)** + - Entire global frame (header/nav/footer) is on blue/slate palette utilities + - creates visual disconnect from tokenized warm palette used in recipe surfaces + +2. **`CookModePage.tsx`** + - Separate visual language (blue/gray success/error blocks) and no `ui-*` primitives + - likely to regress independently if token theme changes + +3. **`ImportUrlPage.tsx`** + - Similar drift pattern to CookModePage; raw utility palette + custom gradients + +4. **`RecipeDetailPage.tsx`** + - Mixed approach in same component: token vars + ui-* + raw palette + inline styles + - hard to predict and maintain consistency + +5. **`Toast.tsx` + `theme.ts` coupling** + - toast visuals encoded in TS style objects instead of ui primitives/token classes + +6. **`App.css` + legacy selectors in `index.css`** + - `App.css` appears unused yet present + - `.card` / `.shadow-card` coexist with `ui-card`, increasing ambiguity + +--- + +## 5) Highest-risk problem areas + +- **Token drift risk**: same values duplicated in `tokens.css` and `theme.ts`. +- **App-shell inconsistency risk**: `App.tsx` not on design-system primitives. +- **Page-level divergence risk**: CookMode/Import URL/NotFound/MissionControl use different style dialect than core recipe pages. +- **Maintainability risk**: inline style overrides (`borderRadius`, `boxShadow`) bypass primitive contract and encourage per-component customization. +- **Regression risk**: no guardrails prevent new raw palette utilities in future UI work. + +--- + +## 6) First conversion targets (for stabilization wave) + +1. `src/App.tsx` (global shell contract) +2. `src/components/MissionControlPanel.tsx` (small, easy, high-visibility drift) +3. `src/pages/CookModePage.tsx` (largest isolated drift surface) +4. `src/pages/ImportUrlPage.tsx` (same drift pattern as CookMode) +5. `src/components/Toast.tsx` (inline-style dependency cleanup) + +--- + +## 7) Acceptance criteria for stabilization work (T02+) + +A stabilization pass is accepted when all are true: + +1. **Single token authority** + - CSS vars in `tokens.css` are canonical. + - `theme.ts` no longer independently defines conflicting visual token values. + +2. **Primitive contract adoption** + - app shell + major pages use shared `ui-*` primitives (or wrapper components built from them). + - new features avoid ad-hoc color utilities outside tokenized scheme. + +3. **Inline-style reduction** + - no routine radius/shadow/color inline overrides where equivalent primitive/token class exists. + +4. **Legacy debt disposition** + - `App.css` removed or explicitly retained with documented rationale. + - overlapping legacy helpers (`.card`, `.shadow-card`) either standardized or deprecated. + +5. **Governance check** + - lightweight styling checklist/doc exists and is referenced in workflow. + +--- + +## 8) Suggested next task after T01 + +**T02 — Canonical Token Source Lock** +- lock CSS tokens as source of truth +- redefine `theme.ts` as typed accessor only (or trim to non-visual helpers) +- document token contract before broad conversion starts diff --git a/.harness/docs/styling-stabilization-qa.md b/.harness/docs/styling-stabilization-qa.md new file mode 100644 index 0000000..fed1d49 --- /dev/null +++ b/.harness/docs/styling-stabilization-qa.md @@ -0,0 +1,132 @@ +# T07 — Styling Stabilization QA Pass + +Date: 2026-03-28 +Task: **T07 — Stabilization QA Pass** +Scope: QA review of T02–T06 outcomes in `recipe-manager/frontend` + +## What was reviewed + +Read docs: +- `.harness/styling-stabilization-execution-board.md` +- `.harness/docs/styling-inventory.md` +- `.harness/docs/styling-token-contract.md` +- `.harness/docs/ui-primitive-contract.md` +- `.harness/docs/styling-governance.md` +- `.harness/docs/t05-legacy-style-debt-cleanup.md` +- `docs/visual-audit/after/homepage-qa-note-t04.md` + +Inspected implementation surfaces: +- `src/styles/tokens.css` +- `src/theme.ts` +- `src/index.css` +- `src/components/ui/primitives.tsx` +- `src/App.tsx` +- `src/components/MissionControlPanel.tsx` +- `src/pages/RecipeListPage.tsx` +- `src/pages/CookModePage.tsx` +- `src/pages/ImportUrlPage.tsx` +- `src/components/RecipeCard.tsx` +- `src/components/Toast.tsx` +- `src/components/ErrorBoundary.tsx` +- `src/pages/NotFoundPage.tsx` +- `scripts/style-guardrails.mjs` +- `frontend/package.json` + +Validation commands: +- `npm run style:guardrails` ✅ pass +- `npm run build` ✅ pass +- `npm run lint` ❌ fails (pre-existing non-styling issues; see findings) +- grep scan for raw palette utilities in app/page/component surfaces + +--- + +## QA findings by severity + +### High +1. **Known drift still present in unguarded surfaces (`ErrorBoundary`, `NotFoundPage`)** + - `src/components/ErrorBoundary.tsx` still uses raw Tailwind palette utilities (`bg-gray-*`, `text-gray-*`, `bg-blue-*`, `text-red-*`). + - `src/pages/NotFoundPage.tsx` still uses raw palette (`text-gray-*`, `bg-blue-*`). + - Risk: these are user-visible shell/error states and create immediate visual inconsistency with tokenized surfaces. + +### Medium +2. **Guardrail scope is still too narrow for current stabilized claims** + - Current `style:guardrails` only enforces: + - `src/App.tsx` + - `src/components/MissionControlPanel.tsx` + - `src/components/ui/primitives.tsx` + - High-drift but now partially stabilized files (e.g., `RecipeListPage`, `ImportUrlPage`, `CookModePage`, `RecipeCard`) are not protected yet. + +3. **Residual non-token palette usage remains in otherwise stabilized pages/components** + - `src/pages/RecipeListPage.tsx`: hero overlay gradient uses `from-slate-900...` / `via-slate-900...`. + - `src/components/RecipeCard.tsx`: card image overlay uses `from-slate-950...` / `via-slate-900...`. + - `src/components/Toast.tsx`: close button hover uses `hover:text-gray-200`. + - These are limited, but they are still direct palette leakage. + +4. **Release hygiene risk: lint gate is red** + - `npm run lint` fails due to existing React/TS lint errors in `App.tsx`, `CookModePage.tsx`, `RecipeDetailPage.tsx`, `api.ts`, `types/api-aux.ts`. + - Not strictly a styling blocker, but it weakens governance confidence and CI-readiness. + +### Low +5. **`theme.ts` still contains non-token hex values in `recipeAccentPalette`** + - Most token drift was eliminated correctly, but this transitional array still includes raw hex values. + - This is documented as an exception in T02; low-risk but still outside ideal contract purity. + +--- + +## What is now considered stabilized + +- **Token source authority:** `tokens.css` is effectively canonical; `theme.ts` now primarily references CSS vars instead of duplicating hardcoded token values. +- **Primitive layer exists and is adopted:** `UiPage`, `UiSection`, `UiCard`, `UiButton`, `UiChip`, `UiBadge`, `cn()` are implemented and used in key shell/surface paths. +- **App shell conversion landed:** `App.tsx` now follows tokenized/primitive-aligned styling patterns. +- **Mission Control conversion landed:** `MissionControlPanel.tsx` moved onto tokenized primitive usage. +- **Legacy cleanup landed:** `src/App.css` removed; legacy `.card` alias removed from `index.css`. +- **Governance baseline landed:** written governance + runnable guardrail command in package scripts. + +--- + +## What still needs work + +1. **Close remaining obvious drift surfaces** + - Convert `ErrorBoundary` and `NotFoundPage` to tokenized/primitive patterns. + +2. **Finish palette cleanup on media overlays/edge controls** + - Replace remaining `slate-*`/`gray-*` utility references in `RecipeListPage`, `RecipeCard`, and `Toast` with semantic tokens. + +3. **Expand guardrail coverage in phases** + - Add stabilized pages/components to `scripts/style-guardrails.mjs` scope incrementally (start with `NotFoundPage`, `ErrorBoundary`, `RecipeListPage`, `RecipeCard`, `Toast`). + +4. **Address lint debt to improve release confidence** + - Resolve outstanding lint errors (especially `react-hooks/set-state-in-effect` and explicit `any` violations) so governance checks can become stronger CI gates. + +5. **Optional contract hardening** + - Replace non-token hexes in `recipeAccentPalette` with token references. + +--- + +## Recommended next execution-board task + +**T08 (new): Guardrail Scope Expansion + Residual Drift Cleanup** + +Proposed scope: +- Convert `ErrorBoundary` + `NotFoundPage` to tokenized primitives. +- Remove remaining raw palette classes in `RecipeListPage`, `RecipeCard`, `Toast`. +- Expand `style:guardrails` file scope to include these surfaces. +- Keep changes low-risk and styling-only. + +If no new task id is allowed, treat this as **T07 follow-on patch set** before final signoff. + +--- + +## Release gate status (from execution board) + +- [x] One canonical token source with no major CSS/TS drift +- [x] Base primitives centralized and used across high-traffic surfaces +- [~] No new ad-hoc color classes outside tokenized set (mostly true, residual exceptions remain) +- [x] Legacy scaffold artifacts removed/documented +- [x] Governance doc + guardrail command exist + +Overall: **Near-stable, not fully closed** due to residual palette drift and narrow guardrail scope. + +## Ready for review + +**Yes (with follow-on cleanup required before final “fully stabilized” signoff).** diff --git a/.harness/docs/styling-token-contract.md b/.harness/docs/styling-token-contract.md new file mode 100644 index 0000000..4c6d358 --- /dev/null +++ b/.harness/docs/styling-token-contract.md @@ -0,0 +1,50 @@ +# Styling Token Contract (T02) + +Date: 2026-03-27 +Scope: `recipe-manager/frontend` + +## Canonical token source + +**Single source of truth:** +- `src/styles/tokens.css` + +All design-token values (color, typography scales, spacing, radius, elevation/focus) must be authored in `tokens.css` first. + +## Allowed token consumption paths + +1. **Preferred in UI markup:** tokenized classes and `ui-*` primitives from `src/index.css` + - Examples: `ui-card`, `ui-btn`, `text-[var(--text)]`, `border-[var(--border)]` +2. **Tailwind theme keys** mapped to CSS vars in `tailwind.config.js` + - Examples: `bg-surface`, `text-primary`, `shadow-card` +3. **TS access layer:** `src/theme.ts` + - Must expose `var(--...)` references only. + - Must not hardcode competing token values. + +## Explicit non-contract patterns + +- Do **not** introduce new hardcoded design-token values in `theme.ts`. +- Do **not** define duplicate token constants in TS that can drift from `tokens.css`. +- Avoid ad-hoc palette classes (`bg-blue-*`, `text-slate-*`, etc.) in shared shell/feature UI when tokenized equivalents exist. + +## `theme.ts` role after T02 + +`theme.ts` is now a **typed token accessor/compatibility layer**, not an independent token definition file. + +- ✅ Allowed: `colors.primary = 'var(--color-primary)'` +- ❌ Not allowed: `colors.primary = '#ea580c'` + +If a token does not exist yet: +1. Add it to `tokens.css` +2. (Optional) Map it in `tailwind.config.js` if utility-class access is needed +3. Expose accessor in `theme.ts` only as `var(--token-name)` + +## Practical authoring guide (for future tasks) + +- Use `ui-*` classes first for common controls/layout shells. +- Use tokenized Tailwind utilities or `var(--...)` references for one-off styling. +- Keep inline `style={{ ... }}` for runtime/dynamic values only (e.g., tag color from DB), not for static design tokens. + +## Current known exceptions (post-T02) + +- Some components/pages still import `radius`/`colors` from `theme.ts` for inline styling; values are now token references, so no hardcoded drift remains. +- `recipeAccentPalette` still includes a few non-tokenized accent hexes pending a future semantic palette pass. diff --git a/.harness/docs/t05-legacy-style-debt-cleanup.md b/.harness/docs/t05-legacy-style-debt-cleanup.md new file mode 100644 index 0000000..ca66049 --- /dev/null +++ b/.harness/docs/t05-legacy-style-debt-cleanup.md @@ -0,0 +1,40 @@ +# T05 — Legacy Style Debt Cleanup + +Date: 2026-03-27 +Project: `recipe-manager/frontend` + +## Scope +Conservative cleanup of clearly obsolete style artifacts after token/primitive contract stabilization. + +## Changes made + +### 1) Removed unused legacy scaffold stylesheet +- Deleted: `frontend/src/App.css` +- Rationale: file contained Vite starter/demo selectors (`.hero`, `.counter`, `#next-steps`, etc.) and is not imported by `main.tsx` or `App.tsx`. +- Risk: low (no import path, no app references). + +### 2) Removed dead legacy helper selector from global stylesheet +- Updated: `frontend/src/index.css` +- Removed legacy `.card` selector alias block and kept `.shadow-card` (still actively used). +- Before: `.card, .shadow-card { ... }` plus `.shadow-card { ... }` +- After: `.shadow-card { ... }` only. +- Rationale: avoid duplicate/ambiguous card styling pathways now that `ui-card` is the primitive contract. + +## Validation +- Verified no `App.css` imports in current frontend source. +- Verified no `className="card"` usage in current frontend source. +- Build check: `npm run build` passes. + +## Deferred legacy debt (intentionally not removed) +1. **`.shadow-card` utility** in `index.css` — still used across pages/components (`RecipeForm`, `RecipeListPage`, `RecipeDetailPage`), so not safe to remove in T05. +2. **`.animate-slide-in` + `@keyframes slide-in`** — used by `components/Toast.tsx`; retained. +3. **Residual inline style usage from `theme.ts`** (e.g., `Toast`, `RecipeCard`) — not removed here to avoid broader UI behavior changes; belongs to follow-up conversion task. +4. **Responsive global utility overrides in `index.css` (`.max-w-*`, `.p-*`)** — potentially legacy/scaffold-ish but high blast-radius; defer pending design QA. + +## Recommendation (next task) +Proceed with **T06 — Guardrails + Lightweight Governance**: +- codify that new UI must use `ui-*`/primitives and tokenized vars, +- add a checklist to prevent re-introducing dead aliases and raw palette drift. + +## Ready for review +**Yes** diff --git a/.harness/docs/ui-primitive-contract.md b/.harness/docs/ui-primitive-contract.md new file mode 100644 index 0000000..c6b30e5 --- /dev/null +++ b/.harness/docs/ui-primitive-contract.md @@ -0,0 +1,91 @@ +# T03 — UI Primitive Contract (Recipe Manager UI Kit) + +Date: 2026-03-27 +Scope: `recipe-manager/frontend` + +## Goal +Stabilize core UI primitives so pages stop re-implementing button/card/chip/layout class strings. + +This is a **low-risk contract layer**, not a redesign system. + +--- + +## Contract surface (v1) + +Implemented in: +- `frontend/src/components/ui/cn.ts` +- `frontend/src/components/ui/primitives.tsx` + +### `cn(...values)` +- Lightweight class composer used by primitives and page-level variant helpers. +- Avoids repetitive template-string class logic. + +### `UiPage` +- Canonical page shell wrapper (`ui-page` + optional additions). +- Use for top-level layout regions (header container, footer container, page root if needed). + +### `UiSection` +- Canonical section shell (`ui-section`) with controlled padding options: + - `padding="md"` (default) + - `padding="lg"` + - `padding="none"` +- Use for grouped content blocks and major page sections. + +### `UiCard` +- Canonical card surface (`ui-card`) with optional tone: + - `tone="default"` + - `tone="muted"` +- Use for repeatable content cards and skeleton containers. + +### `UiButton` +- Canonical button primitive (`ui-btn` + variant mapping): + - `variant="primary"` + - `variant="secondary"` (default) +- Use for interactive ` + +Vegetarian +``` + +## 7) Notes for next tasks (T04+) +- Reuse these primitives before introducing one-off visual patterns. +- Keep all new colors mapped to semantic tokens. +- Preserve AA contrast and focus visibility for every interactive control. + +## 8) Imagery and icon direction update (Photo-First) +- Food storytelling should use real photography as the default medium. +- Legacy custom illustrations are fallback/decorative only. +- Prefer photographic aspect ratios: + - list cards: 4:3 + - detail hero: 16:9 (or 3:2) + - square thumbnails: 1:1 +- Keep icons modern and restrained; standardize on `lucide-react` for action/navigation icons. +- Avoid emoji icons in production UI where a system icon exists. + +See also: `docs/visual-audit/photo-first-assets.md` and `frontend/public/assets/photos/manifest.json`. diff --git a/fix-image-paths.js b/fix-image-paths.js new file mode 100644 index 0000000..c057c13 --- /dev/null +++ b/fix-image-paths.js @@ -0,0 +1,43 @@ +#!/usr/bin/env node +import initSqlJs from 'sql.js'; +import fs from 'fs'; +import path from 'path'; + +const DB_PATH = 'data/recipes.db'; + +async function fixImagePaths() { + const SQL = await initSqlJs(); + const dbBuffer = fs.readFileSync(DB_PATH); + const db = new SQL.Database(dbBuffer); + + // Get all recipes with image_url starting with 'images/' + const recipes = db.exec(` + SELECT id, title, image_url + FROM recipes + WHERE image_url LIKE 'images/%' + `); + + if (recipes.length === 0 || recipes[0].values.length === 0) { + console.log('✓ No image paths to update'); + return; + } + + let updated = 0; + recipes[0].values.forEach(([id, title, imageUrl]) => { + // Change 'images/file.jpg' to '/images/file.jpg' + const newUrl = '/' + imageUrl; + + db.run(`UPDATE recipes SET image_url = ? WHERE id = ?`, [newUrl, id]); + console.log(`✓ Updated recipe ${id} (${title}): ${imageUrl} → ${newUrl}`); + updated++; + }); + + // Save the database + const data = db.export(); + fs.writeFileSync(DB_PATH, data); + db.close(); + + console.log(`\n✓ Fixed ${updated} image paths`); +} + +fixImagePaths().catch(console.error); diff --git a/frontend/README.md b/frontend/README.md index 57d4ee9..80662cd 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -26,6 +26,10 @@ npm run preview # Lint code npm run lint + +# Style governance checks (token/primitive drift guardrails) +npm run style:guardrails +npm run style:check ``` ## Project Structure @@ -60,3 +64,13 @@ See `/ARCHITECTURE.md` for full system architecture and patterns. - The homepage hero uses a fallback chain: bundled `hero.png` first, then `/images/hero-fallback.svg`. - Keep fallback assets lightweight (SVG preferred) and store browser-served fallbacks under `public/images/`. - Any new UI-critical image should follow the same fallback pattern to avoid broken-image regressions in production. + +## Styling Guardrails + +This frontend uses a token + primitive styling contract. + +- Governance doc: `../.harness/docs/styling-governance.md` +- Token contract: `../.harness/docs/styling-token-contract.md` +- Primitive contract: `../.harness/docs/ui-primitive-contract.md` + +Use `npm run style:guardrails` before PRs touching app shell/shared UI surfaces. diff --git a/frontend/package.json b/frontend/package.json index 6b0bcb2..626d8fe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,7 +7,9 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "style:guardrails": "node ./scripts/style-guardrails.mjs", + "style:check": "npm run style:guardrails && npm run lint" }, "dependencies": { "react": "^19.2.4", diff --git a/frontend/public/assets/README.md b/frontend/public/assets/README.md index 4c32a3d..384c06c 100644 --- a/frontend/public/assets/README.md +++ b/frontend/public/assets/README.md @@ -1,21 +1,28 @@ -# Visual Asset Pack (T03) +# Frontend Visual Assets -This directory contains **copyright-safe, generated-in-project assets** for the visual redesign. +This folder contains visual assets for Recipe Manager. -## Structure +## Directory map -- `food/` — recipe/photo placeholders for cards, detail hero, and upload dropzone states -- `category/` — category icon SVGs (breakfast, lunch, dinner, dessert, snack) -- `empty-state/` — illustrations for no recipes, no favorites, and no search results -- `ui/` — reserved for additional decorative UI assets +- `photos/` — **new photo-first asset surface** (manifest + future vendored photos) +- `hero/` — legacy illustration hero assets (deprecated for primary storytelling) +- `food/` — placeholders and legacy illustrated food assets +- `food/curated/` — legacy illustrated recipe art (deprecated for cards) +- `category/` — category icons (retain) +- `category/illustrations/` — legacy illustrated category art (deprecated) +- `empty-state/` — empty-state illustrations (retain temporarily) +- `ui/` — shared decorative UI assets -## Usage Notes +## Direction update -- Reference from frontend as absolute public URLs, e.g. `/assets/food/placeholder-recipe.svg` -- Prefer SVG placeholders over external stock images until approved imagery is available -- Keep files lightweight and editable (plain SVG) +As of the photo-first correction wave, **real food photography is the primary visual direction**. +Illustrations are now fallback/support-only unless explicitly approved. -## License Safety +## Source and licensing -All assets in this folder were authored for this project and are safe for internal/commercial use under repository ownership. -No third-party copyrighted graphics are included. +- Legacy SVG pack: original repository-owned artwork, no attribution required. +- Photo-first pipeline: see `photos/README.md` and `photos/manifest.json`. + +Docs: +- `docs/visual-audit/assets.md` +- `docs/visual-audit/photo-first-assets.md` diff --git a/frontend/public/assets/category/illustrations/category-breakfast.svg b/frontend/public/assets/category/illustrations/category-breakfast.svg new file mode 100644 index 0000000..3d549c4 --- /dev/null +++ b/frontend/public/assets/category/illustrations/category-breakfast.svg @@ -0,0 +1 @@ +Breakfast category illustrationOriginal in-project breakfast illustration with sun and plate. \ No newline at end of file diff --git a/frontend/public/assets/category/illustrations/category-dessert.svg b/frontend/public/assets/category/illustrations/category-dessert.svg new file mode 100644 index 0000000..3ed6303 --- /dev/null +++ b/frontend/public/assets/category/illustrations/category-dessert.svg @@ -0,0 +1 @@ +Dessert category illustrationOriginal in-project dessert illustration with cupcake silhouette. \ No newline at end of file diff --git a/frontend/public/assets/category/illustrations/category-dinner.svg b/frontend/public/assets/category/illustrations/category-dinner.svg new file mode 100644 index 0000000..cb4d687 --- /dev/null +++ b/frontend/public/assets/category/illustrations/category-dinner.svg @@ -0,0 +1 @@ +Dinner category illustrationOriginal in-project dinner illustration with cloche and stars. \ No newline at end of file diff --git a/frontend/public/assets/empty-state/no-cook-history.svg b/frontend/public/assets/empty-state/no-cook-history.svg new file mode 100644 index 0000000..c06be58 --- /dev/null +++ b/frontend/public/assets/empty-state/no-cook-history.svg @@ -0,0 +1 @@ +No cooking history illustrationOriginal in-project empty-state illustration for no recent cooking activity. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/bowl-salad.svg b/frontend/public/assets/food/curated/bowl-salad.svg new file mode 100644 index 0000000..3f85365 --- /dev/null +++ b/frontend/public/assets/food/curated/bowl-salad.svg @@ -0,0 +1 @@ +Salad bowl placeholderOriginal in-project illustration representing a fresh salad bowl. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/breakfast-toast.svg b/frontend/public/assets/food/curated/breakfast-toast.svg new file mode 100644 index 0000000..40d8960 --- /dev/null +++ b/frontend/public/assets/food/curated/breakfast-toast.svg @@ -0,0 +1 @@ +Breakfast toast placeholderOriginal in-project illustration of toast and egg breakfast. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/dessert-cake.svg b/frontend/public/assets/food/curated/dessert-cake.svg new file mode 100644 index 0000000..093d787 --- /dev/null +++ b/frontend/public/assets/food/curated/dessert-cake.svg @@ -0,0 +1 @@ +Dessert cake placeholderOriginal in-project illustration of cake slice and berry garnish. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/pasta-plate.svg b/frontend/public/assets/food/curated/pasta-plate.svg new file mode 100644 index 0000000..c9fb355 --- /dev/null +++ b/frontend/public/assets/food/curated/pasta-plate.svg @@ -0,0 +1 @@ +Pasta plate placeholderOriginal in-project illustration representing pasta dish. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/smoothie-glass.svg b/frontend/public/assets/food/curated/smoothie-glass.svg new file mode 100644 index 0000000..06b8fc6 --- /dev/null +++ b/frontend/public/assets/food/curated/smoothie-glass.svg @@ -0,0 +1 @@ +Smoothie glass placeholderOriginal in-project illustration of fruit smoothie drink. \ No newline at end of file diff --git a/frontend/public/assets/food/curated/soup-bowl.svg b/frontend/public/assets/food/curated/soup-bowl.svg new file mode 100644 index 0000000..7aefbca --- /dev/null +++ b/frontend/public/assets/food/curated/soup-bowl.svg @@ -0,0 +1 @@ +Soup bowl placeholderOriginal in-project illustration of warm soup bowl. \ No newline at end of file diff --git a/frontend/public/assets/hero/hero-fresh-produce.svg b/frontend/public/assets/hero/hero-fresh-produce.svg new file mode 100644 index 0000000..1fc7a89 --- /dev/null +++ b/frontend/public/assets/hero/hero-fresh-produce.svg @@ -0,0 +1,25 @@ + + Fresh produce hero illustration + Original abstract produce composition with leafy accents and platter for recipe app hero usage. + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/hero/hero-kitchen-light.svg b/frontend/public/assets/hero/hero-kitchen-light.svg new file mode 100644 index 0000000..c7ef503 --- /dev/null +++ b/frontend/public/assets/hero/hero-kitchen-light.svg @@ -0,0 +1,28 @@ + + Kitchen prep hero illustration + Original abstract kitchen-themed hero illustration with produce, bowl and utensils. + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/assets/photos/README.md b/frontend/public/assets/photos/README.md new file mode 100644 index 0000000..74d25f8 --- /dev/null +++ b/frontend/public/assets/photos/README.md @@ -0,0 +1,34 @@ +# Photo-First Assets (Wave 2.5) + +This directory is the handoff surface for migrating from illustration-first to photo-first art direction. + +## Current status + +- `manifest.json` is the source of truth for photo slots and candidate sources. +- No third-party photos are vendored yet in this repo. +- Existing SVG placeholders remain as safe local fallbacks until approved photos are added. + +## Folder intent + +- `hero/` — homepage/detail hero photos (`.jpg`/`.webp`) +- `list/` — 4:3 and 1:1 card/list photos +- `detail/` — 16:9 or 3:2 detail hero photos +- `category/` — optional category cover photos +- `empty-state/` — optional minimal photographic empty-state support +- `credits/` — attribution and source snapshots when required + +## Licensing guardrails + +Preferred source order: +1. **Pexels** (free commercial use, attribution typically not required) +2. **Unsplash** (free use under Unsplash License, attribution appreciated) +3. **Owned/internal photos** (full control) + +Rules: +- Verify license at time of download and record it in `manifest.json`. +- Never ingest images marked editorial-only or with recognizable private persons without release confidence. +- Keep `sourceUrl`, `license`, and `downloadedAt` metadata for every vendored photo. + +## Integration + +UI workers should consume `manifest.json` and gracefully fallback to local placeholders when `localPath` is null. diff --git a/frontend/public/assets/photos/category/.gitkeep b/frontend/public/assets/photos/category/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/credits/.gitkeep b/frontend/public/assets/photos/credits/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/detail/.gitkeep b/frontend/public/assets/photos/detail/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/empty-state/.gitkeep b/frontend/public/assets/photos/empty-state/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/hero/.gitkeep b/frontend/public/assets/photos/hero/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/list/.gitkeep b/frontend/public/assets/photos/list/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/frontend/public/assets/photos/manifest.json b/frontend/public/assets/photos/manifest.json new file mode 100644 index 0000000..f8bd026 --- /dev/null +++ b/frontend/public/assets/photos/manifest.json @@ -0,0 +1,76 @@ +{ + "version": 1, + "updatedAt": "2026-03-27", + "notes": "Photo-first manifest for Wave 2.5 UI integration. Do not assume listed external photos are vendored until localPath exists.", + "usage": { + "homepageHero": "assets.photos.hero.homepage.primary", + "listCardFallback": "assets.photos.list.default4x3", + "detailHeroFallback": "assets.photos.detail.default16x9", + "emptyStates": "assets.photos.emptyState" + }, + "assets": { + "photos": { + "hero": { + "homepage": { + "primary": { + "id": "hero-market-produce-01", + "localPath": null, + "remoteCandidate": "https://images.pexels.com/photos/1640774/pexels-photo-1640774.jpeg", + "license": "Pexels License", + "attributionRequired": false, + "status": "candidate-not-vendored" + }, + "fallback": [ + "/assets/food/placeholder-recipe.svg" + ] + } + }, + "list": { + "default4x3": "/assets/food/placeholder-recipe-4x3.svg", + "default1x1": "/assets/food/placeholder-recipe-1x1.svg" + }, + "detail": { + "default16x9": "/assets/food/placeholder-recipe.svg" + }, + "category": { + "breakfast": { + "localPath": null, + "remoteCandidate": "https://images.pexels.com/photos/70497/pexels-photo-70497.jpeg", + "license": "Pexels License", + "attributionRequired": false, + "status": "candidate-not-vendored" + }, + "dinner": { + "localPath": null, + "remoteCandidate": "https://images.pexels.com/photos/1279330/pexels-photo-1279330.jpeg", + "license": "Pexels License", + "attributionRequired": false, + "status": "candidate-not-vendored" + }, + "dessert": { + "localPath": null, + "remoteCandidate": "https://images.pexels.com/photos/291528/pexels-photo-291528.jpeg", + "license": "Pexels License", + "attributionRequired": false, + "status": "candidate-not-vendored" + } + }, + "emptyState": { + "noRecipes": "/assets/empty-state/no-recipes.svg", + "noResults": "/assets/empty-state/no-results-search.svg", + "noFavorites": "/assets/empty-state/no-favorites.svg" + } + }, + "icons": { + "recommendedLibrary": "lucide-react", + "size": { + "control": 18, + "section": 24 + }, + "strokeWidth": { + "default": 1.75 + }, + "policy": "Use icons for navigation/actions only. Do not use illustrated iconography for food storytelling." + } + } +} diff --git a/frontend/scripts/style-guardrails.mjs b/frontend/scripts/style-guardrails.mjs new file mode 100644 index 0000000..7f1d1e9 --- /dev/null +++ b/frontend/scripts/style-guardrails.mjs @@ -0,0 +1,76 @@ +#!/usr/bin/env node + +import fs from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +const projectRoot = process.cwd(); + +const scopedFiles = [ + 'src/App.tsx', + 'src/components/MissionControlPanel.tsx', + 'src/components/ui/primitives.tsx', + 'src/components/ErrorBoundary.tsx', + 'src/pages/NotFoundPage.tsx', + 'src/pages/RecipeListPage.tsx', + 'src/components/RecipeCard.tsx', + 'src/components/Toast.tsx', +].map((file) => path.resolve(projectRoot, file)); + +const bannedPatterns = [ + { + name: 'raw-tailwind-palette', + regex: + /\b(?:bg|text|border|ring|from|to|via|outline|decoration)-(?:slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-\d{2,3}\b/g, + message: + 'Use tokenized classes/variables instead of direct Tailwind palette utilities (e.g. bg-slate-200, text-blue-600).', + }, + { + name: 'hex-in-arbitrary-class', + regex: /\b(?:bg|text|border|ring|from|to|via)-\[#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})\]\b/g, + message: + 'Avoid hardcoded hex colors in className. Add/consume a semantic token in tokens.css instead.', + }, +]; + +function readFile(filePath) { + return fs.readFileSync(filePath, 'utf8'); +} + +function lineNumberAt(text, index) { + return text.slice(0, index).split('\n').length; +} + +const findings = []; + +for (const filePath of scopedFiles) { + if (!fs.existsSync(filePath)) continue; + const content = readFile(filePath); + + for (const rule of bannedPatterns) { + for (const match of content.matchAll(rule.regex)) { + findings.push({ + filePath, + line: lineNumberAt(content, match.index ?? 0), + value: match[0], + rule: rule.name, + message: rule.message, + }); + } + } +} + +if (findings.length === 0) { + console.log('✅ style-guardrails: no blocked raw palette patterns found in scoped files.'); + process.exit(0); +} + +console.error('❌ style-guardrails: found styling drift patterns in guarded files:\n'); +for (const finding of findings) { + const relativePath = path.relative(projectRoot, finding.filePath); + console.error(`- ${relativePath}:${finding.line} [${finding.rule}] ${finding.value}`); + console.error(` ${finding.message}`); +} + +console.error('\nSee .harness/docs/styling-governance.md for the contract and escalation path.'); +process.exit(1); diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index f90339d..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,184 +0,0 @@ -.counter { - font-size: 16px; - padding: 5px 10px; - border-radius: 5px; - color: var(--accent); - background: var(--accent-bg); - border: 2px solid transparent; - transition: border-color 0.3s; - margin-bottom: 24px; - - &:hover { - border-color: var(--accent-border); - } - &:focus-visible { - outline: 2px solid var(--accent); - outline-offset: 2px; - } -} - -.hero { - position: relative; - - .base, - .framework, - .vite { - inset-inline: 0; - margin: 0 auto; - } - - .base { - width: 170px; - position: relative; - z-index: 0; - } - - .framework, - .vite { - position: absolute; - } - - .framework { - z-index: 1; - top: 34px; - height: 28px; - transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg) - scale(1.4); - } - - .vite { - z-index: 0; - top: 107px; - height: 26px; - width: auto; - transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg) - scale(0.8); - } -} - -#center { - display: flex; - flex-direction: column; - gap: 25px; - place-content: center; - place-items: center; - flex-grow: 1; - - @media (max-width: 1024px) { - padding: 32px 20px 24px; - gap: 18px; - } -} - -#next-steps { - display: flex; - border-top: 1px solid var(--border); - text-align: left; - - & > div { - flex: 1 1 0; - padding: 32px; - @media (max-width: 1024px) { - padding: 24px 20px; - } - } - - .icon { - margin-bottom: 16px; - width: 22px; - height: 22px; - } - - @media (max-width: 1024px) { - flex-direction: column; - text-align: center; - } -} - -#docs { - border-right: 1px solid var(--border); - - @media (max-width: 1024px) { - border-right: none; - border-bottom: 1px solid var(--border); - } -} - -#next-steps ul { - list-style: none; - padding: 0; - display: flex; - gap: 8px; - margin: 32px 0 0; - - .logo { - height: 18px; - } - - a { - color: var(--text-h); - font-size: 16px; - border-radius: 6px; - background: var(--social-bg); - display: flex; - padding: 6px 12px; - align-items: center; - gap: 8px; - text-decoration: none; - transition: box-shadow 0.3s; - - &:hover { - box-shadow: var(--shadow); - } - .button-icon { - height: 18px; - width: 18px; - } - } - - @media (max-width: 1024px) { - margin-top: 20px; - flex-wrap: wrap; - justify-content: center; - - li { - flex: 1 1 calc(50% - 8px); - } - - a { - width: 100%; - justify-content: center; - box-sizing: border-box; - } - } -} - -#spacer { - height: 88px; - border-top: 1px solid var(--border); - @media (max-width: 1024px) { - height: 48px; - } -} - -.ticks { - position: relative; - width: 100%; - - &::before, - &::after { - content: ''; - position: absolute; - top: -4.5px; - border: 5px solid transparent; - } - - &::before { - left: 0; - border-left-color: var(--border); - } - &::after { - right: 0; - border-right-color: var(--border); - } -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 55f6cae..386d513 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,6 +8,8 @@ import { ErrorBoundary } from './components/ErrorBoundary'; import { ToastContainer } from './components/Toast'; import { useToast } from './hooks/useToast'; import { createContext, useContext } from 'react'; +import { UiPage } from './components/ui/primitives'; +import { cn } from './components/ui/cn'; interface ToastContextType { success: (message: string, duration?: number) => string; @@ -63,30 +65,30 @@ function App() { return false; }; - const linkClass = (path: string) => { - const base = - 'group/nav relative inline-flex items-center gap-2 rounded-full px-4 py-2 text-sm font-semibold transition-all duration-200 ease-out outline-none focus-visible:ring-2 focus-visible:ring-blue-600 focus-visible:ring-offset-2 focus-visible:ring-offset-white motion-safe:hover:-translate-y-0.5'; - return isActive(path) - ? `${base} bg-blue-100 text-blue-700 shadow-sm` - : `${base} text-slate-700 hover:bg-slate-100 hover:text-slate-900`; - }; + const linkClass = (path: string) => + cn( + 'ui-btn min-h-0 rounded-full border border-transparent px-4 py-2 text-sm font-semibold shadow-none', + isActive(path) + ? 'bg-[var(--color-primary-light)] text-[var(--color-primary-dark)]' + : 'bg-transparent text-[var(--text-dim)] hover:bg-[var(--surface-muted)] hover:text-[var(--text)]', + ); return ( -
+
-
-
+
+
-

Recipe Manager

+

Recipe Manager

-
+
-
+
} /> } /> @@ -118,25 +120,25 @@ function App() {
-