feat(ui): add design tokens and visual style guide (T02)
This commit is contained in:
parent
3931455e64
commit
4af20eaf91
|
|
@ -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).
|
||||
|
|
@ -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) {
|
|||
</div>
|
||||
|
||||
<div className="flex h-[calc(100%-10rem)] min-h-[210px] flex-col p-5">
|
||||
<h3 className="mb-1 line-clamp-2 text-lg font-bold text-gray-900 transition-colors duration-200 group-hover:text-blue-700">{recipe.title}</h3>
|
||||
<h3
|
||||
className="mb-1 line-clamp-2 text-lg font-bold text-gray-900 transition-colors duration-200 group-hover:text-blue-700"
|
||||
style={{ fontSize: typography.fontSize.lg }}
|
||||
>
|
||||
{recipe.title}
|
||||
</h3>
|
||||
|
||||
{recipe.description ? (
|
||||
<p className="mb-3 line-clamp-2 text-sm text-gray-600">{recipe.description}</p>
|
||||
|
|
|
|||
|
|
@ -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<string>(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 (
|
||||
<div className="bg-red-50 border border-red-200 rounded-md p-3">
|
||||
<p className="text-red-700 text-sm">Error loading tags: {error}</p>
|
||||
<div className="rounded-md border border-red-200 bg-red-50 p-3">
|
||||
<p className="text-sm text-red-700">Error loading tags: {error}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -66,25 +67,18 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) {
|
|||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{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 (
|
||||
<button
|
||||
key={tag.id}
|
||||
type="button"
|
||||
onClick={() => handleToggleTag(tag)}
|
||||
className={`
|
||||
px-3 py-1 rounded-full text-sm font-medium transition-colors
|
||||
${isSelected
|
||||
? 'bg-blue-500 text-white'
|
||||
: 'bg-gray-200 text-gray-700 hover:bg-gray-300'
|
||||
}
|
||||
rounded-full px-3 py-1 text-sm font-medium transition-colors
|
||||
${isSelected ? 'bg-blue-500 text-white' : 'bg-gray-200 text-gray-700 hover:bg-gray-300'}
|
||||
`}
|
||||
style={
|
||||
isSelected && tag.color
|
||||
? { backgroundColor: tag.color }
|
||||
: {}
|
||||
}
|
||||
style={isSelected && tag.color ? { backgroundColor: tag.color } : {}}
|
||||
>
|
||||
{tag.name}
|
||||
</button>
|
||||
|
|
@ -96,19 +90,19 @@ export function TagSelector({ selectedTags, onTagsChange }: TagSelectorProps) {
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => setShowNewTagForm(true)}
|
||||
className="text-blue-600 hover:text-blue-700 text-sm font-medium"
|
||||
className="text-sm font-medium text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
+ Create new tag
|
||||
</button>
|
||||
) : (
|
||||
<form onSubmit={handleCreateTag} className="flex gap-2 items-end">
|
||||
<form onSubmit={handleCreateTag} className="flex items-end gap-2">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newTagName}
|
||||
onChange={(e) => 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
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={creating || !newTagName.trim()}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
||||
className="rounded-md bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:cursor-not-allowed disabled:bg-gray-400"
|
||||
>
|
||||
{creating ? 'Creating...' : 'Add'}
|
||||
</button>
|
||||
|
|
@ -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
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div
|
||||
className={`${bgColor} text-white px-6 py-4 rounded-lg shadow-lg flex items-center justify-between gap-4 min-w-[300px] max-w-[500px] animate-slide-in`}
|
||||
className="animate-slide-in flex min-w-[300px] max-w-[500px] items-center justify-between gap-4 px-6 py-4 text-white"
|
||||
style={{
|
||||
backgroundColor,
|
||||
borderRadius: radius.lg,
|
||||
boxShadow: shadows.card,
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-xl font-bold">{icon}</span>
|
||||
<span className="font-medium">{message.message}</span>
|
||||
<span
|
||||
className="font-medium"
|
||||
style={{ fontSize: typography.fontSize.base, lineHeight: typography.lineHeight.normal }}
|
||||
>
|
||||
{message.message}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => onClose(message.id)}
|
||||
className="text-white hover:text-gray-200 text-xl font-bold leading-none"
|
||||
className="text-xl font-bold leading-none text-white transition-colors hover:text-gray-200"
|
||||
style={{ marginInlineStart: spacing.xs }}
|
||||
aria-label="Close"
|
||||
>
|
||||
×
|
||||
|
|
@ -76,7 +88,7 @@ export function ToastContainer({ messages, onClose }: ToastContainerProps) {
|
|||
if (messages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed top-4 right-4 z-50 flex flex-col gap-2">
|
||||
<div className="fixed right-4 top-4 z-50 flex flex-col gap-2">
|
||||
{messages.map((message) => (
|
||||
<Toast key={message.id} message={message} onClose={onClose} />
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,13 @@
|
|||
--color-primary-light: #dbeafe;
|
||||
--color-accent: #9333ea;
|
||||
|
||||
--color-success: #15803d;
|
||||
--color-success-light: #dcfce7;
|
||||
--color-warning: #ca8a04;
|
||||
--color-warning-light: #fef3c7;
|
||||
--color-error: #dc2626;
|
||||
--color-error-light: #fee2e2;
|
||||
|
||||
--text: #1f2937;
|
||||
--text-h: #0f172a;
|
||||
--text-dim: #64748b;
|
||||
|
|
@ -19,9 +26,33 @@
|
|||
--border: #dbe3ef;
|
||||
--code-bg: #eef2f7;
|
||||
|
||||
--radius-xs: 0.375rem;
|
||||
--radius-sm: 0.5rem;
|
||||
--radius-md: 0.75rem;
|
||||
--radius-lg: 1rem;
|
||||
--radius-xl: 1.25rem;
|
||||
|
||||
--space-xxs: 0.25rem;
|
||||
--space-xs: 0.5rem;
|
||||
--space-sm: 0.75rem;
|
||||
--space-md: 1rem;
|
||||
--space-lg: 1.5rem;
|
||||
--space-xl: 2rem;
|
||||
--space-2xl: 2.5rem;
|
||||
--space-3xl: 3rem;
|
||||
|
||||
--font-size-xs: 0.75rem;
|
||||
--font-size-sm: 0.875rem;
|
||||
--font-size-base: 1rem;
|
||||
--font-size-lg: 1.125rem;
|
||||
--font-size-xl: 1.25rem;
|
||||
--font-size-2xl: 1.5rem;
|
||||
--font-size-3xl: 1.875rem;
|
||||
--font-size-4xl: 2.25rem;
|
||||
|
||||
--line-height-tight: 1.2;
|
||||
--line-height-normal: 1.5;
|
||||
--line-height-relaxed: 1.65;
|
||||
|
||||
--shadow-subtle: 0 1px 2px rgba(15, 23, 42, 0.06);
|
||||
--card-shadow: 0 10px 30px rgba(15, 23, 42, 0.08);
|
||||
|
|
@ -71,6 +102,7 @@ body {
|
|||
margin: 0;
|
||||
font-family: var(--sans);
|
||||
color: var(--text);
|
||||
line-height: var(--line-height-normal);
|
||||
background: var(--surface-gradient);
|
||||
background-attachment: fixed;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
/**
|
||||
* Centralized design tokens for the Recipe Manager frontend.
|
||||
* Keep values semantic so UI primitives can evolve without broad refactors.
|
||||
* Global design tokens for the Recipe Manager frontend.
|
||||
*
|
||||
* Keep these semantic and reusable so components can evolve without large refactors.
|
||||
*/
|
||||
|
||||
export const colors = {
|
||||
|
|
@ -8,9 +9,13 @@ export const colors = {
|
|||
primaryDark: '#1d4ed8',
|
||||
primaryLight: '#dbeafe',
|
||||
accent: '#9333ea',
|
||||
|
||||
success: '#15803d',
|
||||
successLight: '#dcfce7',
|
||||
warning: '#ca8a04',
|
||||
warningLight: '#fef3c7',
|
||||
error: '#dc2626',
|
||||
errorLight: '#fee2e2',
|
||||
|
||||
bg: '#f4f7fb',
|
||||
bgAlt: '#edf2f7',
|
||||
|
|
@ -23,7 +28,7 @@ export const colors = {
|
|||
textHeading: '#0f172a',
|
||||
|
||||
focusRing: '#2563eb',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const typography = {
|
||||
fontFamily: {
|
||||
|
|
@ -39,6 +44,7 @@ export const typography = {
|
|||
xl: '1.25rem',
|
||||
'2xl': '1.5rem',
|
||||
'3xl': '1.875rem',
|
||||
'4xl': '2.25rem',
|
||||
},
|
||||
lineHeight: {
|
||||
tight: '1.2',
|
||||
|
|
@ -50,8 +56,9 @@ export const typography = {
|
|||
medium: 500,
|
||||
semibold: 600,
|
||||
bold: 700,
|
||||
extrabold: 800,
|
||||
},
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const spacing = {
|
||||
xxs: '0.25rem',
|
||||
|
|
@ -61,7 +68,8 @@ export const spacing = {
|
|||
lg: '1.5rem',
|
||||
xl: '2rem',
|
||||
'2xl': '2.5rem',
|
||||
};
|
||||
'3xl': '3rem',
|
||||
} as const;
|
||||
|
||||
export const radius = {
|
||||
xs: '0.375rem',
|
||||
|
|
@ -70,11 +78,22 @@ export const radius = {
|
|||
lg: '1rem',
|
||||
xl: '1.25rem',
|
||||
full: '9999px',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const shadows = {
|
||||
subtle: '0 1px 2px rgba(15, 23, 42, 0.06)',
|
||||
card: '0 10px 30px rgba(15, 23, 42, 0.08)',
|
||||
hover: '0 14px 34px rgba(15, 23, 42, 0.12)',
|
||||
focus: '0 0 0 3px rgba(37, 99, 235, 0.25)',
|
||||
};
|
||||
} as const;
|
||||
|
||||
export const recipeAccentPalette = ['#f97316', '#ef4444', '#22c55e', '#06b6d4', '#3b82f6', '#a855f7'] as const;
|
||||
|
||||
export const designTokens = {
|
||||
colors,
|
||||
typography,
|
||||
spacing,
|
||||
radius,
|
||||
shadows,
|
||||
recipeAccentPalette,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -1,35 +1,60 @@
|
|||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
'./index.html',
|
||||
'./src/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
borderRadius: {
|
||||
xs: '4px',
|
||||
sm: '6px',
|
||||
md: '10px',
|
||||
lg: '16px',
|
||||
full: '999px',
|
||||
},
|
||||
boxShadow: {
|
||||
card: '0 2px 8px 0 rgba(28,30,34,0.08)',
|
||||
hover: '0 4px 20px 0 rgba(28,30,34,0.16)',
|
||||
},
|
||||
colors: {
|
||||
primary: '#2563eb',
|
||||
accent: '#aa3bff',
|
||||
success: '#16a34a',
|
||||
warning: '#eab308',
|
||||
error: '#dc2626',
|
||||
primary: 'var(--color-primary)',
|
||||
accent: 'var(--color-accent)',
|
||||
success: 'var(--color-success)',
|
||||
warning: 'var(--color-warning)',
|
||||
error: 'var(--color-error)',
|
||||
surface: 'var(--surface)',
|
||||
muted: 'var(--surface-muted)',
|
||||
border: 'var(--border)',
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['system-ui', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
heading: ['system-ui', 'Segoe UI', 'Roboto', 'sans-serif'],
|
||||
mono: ['ui-monospace', 'Consolas', 'monospace'],
|
||||
sans: ['var(--sans)'],
|
||||
heading: ['var(--heading)'],
|
||||
mono: ['var(--mono)'],
|
||||
},
|
||||
fontSize: {
|
||||
xs: ['var(--font-size-xs)', { lineHeight: 'var(--line-height-normal)' }],
|
||||
sm: ['var(--font-size-sm)', { lineHeight: 'var(--line-height-normal)' }],
|
||||
base: ['var(--font-size-base)', { lineHeight: 'var(--line-height-normal)' }],
|
||||
lg: ['var(--font-size-lg)', { lineHeight: 'var(--line-height-normal)' }],
|
||||
xl: ['var(--font-size-xl)', { lineHeight: 'var(--line-height-tight)' }],
|
||||
'2xl': ['var(--font-size-2xl)', { lineHeight: 'var(--line-height-tight)' }],
|
||||
'3xl': ['var(--font-size-3xl)', { lineHeight: 'var(--line-height-tight)' }],
|
||||
'4xl': ['var(--font-size-4xl)', { lineHeight: 'var(--line-height-tight)' }],
|
||||
},
|
||||
spacing: {
|
||||
xxs: 'var(--space-xxs)',
|
||||
xs: 'var(--space-xs)',
|
||||
sm: 'var(--space-sm)',
|
||||
md: 'var(--space-md)',
|
||||
lg: 'var(--space-lg)',
|
||||
xl: 'var(--space-xl)',
|
||||
'2xl': 'var(--space-2xl)',
|
||||
'3xl': 'var(--space-3xl)',
|
||||
},
|
||||
borderRadius: {
|
||||
xs: 'var(--radius-xs)',
|
||||
sm: 'var(--radius-sm)',
|
||||
md: 'var(--radius-md)',
|
||||
lg: 'var(--radius-lg)',
|
||||
xl: 'var(--radius-xl)',
|
||||
full: '9999px',
|
||||
},
|
||||
boxShadow: {
|
||||
subtle: 'var(--shadow-subtle)',
|
||||
card: 'var(--card-shadow)',
|
||||
hover: 'var(--shadow-hover)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue