diff --git a/docs/visual-audit/visual-style-guide.md b/docs/visual-audit/visual-style-guide.md new file mode 100644 index 0000000..9326251 --- /dev/null +++ b/docs/visual-audit/visual-style-guide.md @@ -0,0 +1,139 @@ +# Visual Style Guide (T02) + +## Purpose +This document defines the Recipe Manager visual design tokens and how to apply them consistently across pages/components. + +Primary source of truth: +- `frontend/src/theme.ts` +- `frontend/src/index.css` (CSS custom properties) +- `frontend/tailwind.config.js` (Tailwind token mapping) + +--- + +## 1) Color Tokens + +### Brand + Semantic +- `colors.primary`: `#2563eb` +- `colors.primaryDark`: `#1d4ed8` +- `colors.primaryLight`: `#dbeafe` +- `colors.accent`: `#9333ea` +- `colors.success`: `#15803d` +- `colors.warning`: `#ca8a04` +- `colors.error`: `#dc2626` + +### Surfaces + Text +- `colors.bg`: `#f4f7fb` +- `colors.bgAlt`: `#edf2f7` +- `colors.surface`: `#ffffff` +- `colors.surfaceMuted`: `#f8fafc` +- `colors.border`: `#dbe3ef` +- `colors.text`: `#1f2937` +- `colors.textHeading`: `#0f172a` +- `colors.textDim`: `#64748b` + +### Usage +- Primary actions: `primary` / `primaryDark` for hover. +- Validation or status badges/alerts: `success`, `warning`, `error` with light backgrounds where needed. +- Use `surface` + `border` for cards/forms. +- Use `textHeading` for section/page headings and `text`/`textDim` for body/supporting copy. + +--- + +## 2) Typography Tokens + +### Families +- Sans: `typography.fontFamily.sans` +- Heading: `typography.fontFamily.heading` +- Mono: `typography.fontFamily.mono` + +### Scale +- `xs` 0.75rem +- `sm` 0.875rem +- `base` 1rem +- `lg` 1.125rem +- `xl` 1.25rem +- `2xl` 1.5rem +- `3xl` 1.875rem +- `4xl` 2.25rem + +### Guidance +- Page titles: `3xl–4xl` +- Section titles: `xl–2xl` +- Body text: `base` +- Helper/meta text: `sm` + +--- + +## 3) Spacing Tokens + +- `xxs` 0.25rem +- `xs` 0.5rem +- `sm` 0.75rem +- `md` 1rem +- `lg` 1.5rem +- `xl` 2rem +- `2xl` 2.5rem +- `3xl` 3rem + +### Guidance +- Tight control spacing (chips/icons): `xxs–xs` +- Form controls/content clusters: `sm–md` +- Section/page spacing: `lg–2xl` + +--- + +## 4) Radius Tokens + +- `xs` 0.375rem +- `sm` 0.5rem +- `md` 0.75rem +- `lg` 1rem +- `xl` 1.25rem +- `full` 9999px + +### Guidance +- Inputs/buttons: `md` +- Cards/containers: `lg` +- Pills/tags: `full` + +--- + +## 5) Shadow Tokens + +- `shadows.subtle`: low emphasis hover/elevation +- `shadows.card`: default card elevation +- `shadows.hover`: raised interactive state +- `shadows.focus`: focus ring treatment + +### Guidance +- Prefer `card` for panels/surfaces. +- Use `subtle` for lightweight interactive surfaces. +- Keep `hover` limited to strong CTAs/cards. + +--- + +## 6) Tailwind Mapping + +Tailwind config maps tokenized CSS variables for: +- `colors` (`primary`, `accent`, `success`, `warning`, `error`, `surface`, `muted`, `border`) +- `fontFamily` +- `fontSize` +- `spacing` +- `borderRadius` +- `boxShadow` + +This keeps utility classes aligned with global tokens and avoids hardcoding values in component markup. + +--- + +## 7) Implementation Notes (T02) + +Updated to consume tokens where practical: +- `frontend/src/theme.ts`: expanded token definitions and shared `designTokens` export. +- `frontend/src/index.css`: added token-backed CSS variables (colors, type scale, spacing, radius, shadows). +- `frontend/tailwind.config.js`: switched extension values to CSS-variable/token-backed mappings. +- `frontend/src/components/Toast.tsx`: semantic status colors + radius/shadow from tokens. +- `frontend/src/components/RecipeCard.tsx`: recipe accent palette sourced from tokens. +- `frontend/src/components/TagSelector.tsx`: default tag color sourced from tokens. + +Scope intentionally kept minimal/non-breaking to support upcoming visual tasks (T04–T07). diff --git a/frontend/src/components/RecipeCard.tsx b/frontend/src/components/RecipeCard.tsx index bc179ef..a43a023 100644 --- a/frontend/src/components/RecipeCard.tsx +++ b/frontend/src/components/RecipeCard.tsx @@ -1,6 +1,6 @@ import { Link } from 'react-router-dom'; import type { Recipe, Tag } from '../types/recipe'; -import { colors, radius, shadows } from '../theme'; +import { colors, radius, recipeAccentPalette, shadows, typography } from '../theme'; interface RecipeCardProps { recipe: Recipe; @@ -25,8 +25,7 @@ function formatDate(timestamp?: number): string { function accentForRecipe(recipe: Recipe, tags: Tag[]) { if (tags[0]?.color) return tags[0].color; - const palette = ['#f97316', '#ef4444', '#22c55e', '#06b6d4', '#3b82f6', '#a855f7']; - return palette[recipe.id % palette.length]; + return recipeAccentPalette[recipe.id % recipeAccentPalette.length]; } function emojiForRecipe(recipe: Recipe) { @@ -68,7 +67,12 @@ export function RecipeCard({ recipe, tags = [] }: RecipeCardProps) {
-

{recipe.title}

+

+ {recipe.title} +

{recipe.description ? (

{recipe.description}

diff --git a/frontend/src/components/TagSelector.tsx b/frontend/src/components/TagSelector.tsx index 94824f4..701b6a4 100644 --- a/frontend/src/components/TagSelector.tsx +++ b/frontend/src/components/TagSelector.tsx @@ -5,6 +5,7 @@ import { useState } from 'react'; import { useTags } from '../hooks/useTags'; import { useToastContext } from '../App'; +import { colors } from '../theme'; import type { Tag } from '../types/recipe'; interface TagSelectorProps { @@ -17,14 +18,14 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { const toast = useToastContext(); const [showNewTagForm, setShowNewTagForm] = useState(false); const [newTagName, setNewTagName] = useState(''); - const [newTagColor, setNewTagColor] = useState('#3B82F6'); + const [newTagColor, setNewTagColor] = useState(colors.primary); const [creating, setCreating] = useState(false); const handleToggleTag = (tag: Tag) => { - const isSelected = selectedTags.some(t => t.id === tag.id); - + const isSelected = selectedTags.some((t) => t.id === tag.id); + if (isSelected) { - onTagsChange(selectedTags.filter(t => t.id !== tag.id)); + onTagsChange(selectedTags.filter((t) => t.id !== tag.id)); } else { onTagsChange([...selectedTags, tag]); } @@ -32,15 +33,15 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { const handleCreateTag = async (e: React.FormEvent) => { e.preventDefault(); - + if (!newTagName.trim()) return; - + setCreating(true); try { const newTag = await addTag(newTagName.trim(), newTagColor); onTagsChange([...selectedTags, newTag]); setNewTagName(''); - setNewTagColor('#3B82F6'); + setNewTagColor(colors.primary); setShowNewTagForm(false); toast.success(`Tag "${newTag.name}" created!`); } catch (err) { @@ -57,8 +58,8 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { if (error) { return ( -
-

Error loading tags: {error}

+
+

Error loading tags: {error}

); } @@ -66,25 +67,18 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { return (
- {tags.map(tag => { - const isSelected = selectedTags.some(t => t.id === tag.id); + {tags.map((tag) => { + const isSelected = selectedTags.some((t) => t.id === tag.id); return ( @@ -96,19 +90,19 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { ) : ( -
+
setNewTagName(e.target.value)} placeholder="Tag name" - className="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500" + className="w-full rounded-md border border-gray-300 px-3 py-2 focus:border-blue-500 focus:ring-2 focus:ring-blue-500" autoFocus />
@@ -117,14 +111,14 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { type="color" value={newTagColor} onChange={(e) => setNewTagColor(e.target.value)} - className="h-10 w-16 border border-gray-300 rounded-md cursor-pointer" + className="h-10 w-16 cursor-pointer rounded-md border border-gray-300" title="Tag color" />
@@ -133,9 +127,9 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) { onClick={() => { setShowNewTagForm(false); setNewTagName(''); - setNewTagColor('#3B82F6'); + setNewTagColor(colors.primary); }} - className="px-4 py-2 bg-gray-200 text-gray-700 rounded-md hover:bg-gray-300" + className="rounded-md bg-gray-200 px-4 py-2 text-gray-700 hover:bg-gray-300" > Cancel diff --git a/frontend/src/components/Toast.tsx b/frontend/src/components/Toast.tsx index a0c8410..96b2a35 100644 --- a/frontend/src/components/Toast.tsx +++ b/frontend/src/components/Toast.tsx @@ -4,6 +4,7 @@ */ import { useEffect } from 'react'; +import { colors, radius, shadows, spacing, typography } from '../theme'; export type ToastType = 'success' | 'error' | 'info' | 'warning'; @@ -31,11 +32,11 @@ export function Toast({ message, onClose }: ToastProps) { return () => clearTimeout(timer); }, [message.id, message.duration, onClose]); - const bgColor = { - success: 'bg-green-600', - error: 'bg-red-600', - info: 'bg-blue-600', - warning: 'bg-yellow-600', + const backgroundColor = { + success: colors.success, + error: colors.error, + info: colors.primary, + warning: colors.warning, }[message.type]; const icon = { @@ -47,15 +48,26 @@ export function Toast({ message, onClose }: ToastProps) { return (
{icon} - {message.message} + + {message.message} +