diff --git a/README.md b/README.md index 244579a..8f74083 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,32 @@ -# Forge +# ⚒️ forge -## Why Forge? +``` +╔═╝╔═║╔═║╔═╝╔═╝ +╔═╝║ ║╔╔╝║ ║╔═╝ +╝ ══╝╝ ╝══╝══╝ +``` -CSS is powerful, but hostile. +## overview -### Problems with CSS +Forge is a typed, local, variant-driven way to organize CSS, built around TSX. -- Styles are **global and open** — anything can override anything. -- There’s **no link** between a class in markup and its definition. -- Inline styles exist because there’s **no structured way to vary styles per instance**. -- Overrides are silent — conflicts happen without feedback. -- Complex components require selector gymnastics and reach-in styling. +## css problems -### What Forge Does Instead +- Styles are global and open - anything can override anything anywhere. +- No IDE-friendly link between the class name in markup and its definition. +- All techniques are patterns a human must know and follow, not APIs. +- Errors happen silently. -- Styles are **local to components** and attached by generated handles, not strings. -- **Parts** give components named sub-targets without selectors. -- **Variants** replace inline styles with typed, declarative parameters. -- Style composition is **deterministic** (known merge order, last-wins). -- Overlapping changes are **warned about in dev**, not silently ignored. +## forge solutions -### What Forge Is +- All styles are local to your TSX components. +- Styles defined using TS typing. +- Component styles are made up of independently styled "Parts". +- "Variants" replace inline styles with typed, declarative parameters. +- Style composition is deterministic. +- Errors and feedback. -- A typed, local, variant-driven way to author CSS. -- A system that optimizes for **people typing at a keyboard**, not selectors in a cascade. - -### What Forge Is Not - -- Not a new component model. -- Not a new language. -- Not a CSS replacement — it compiles _to_ CSS, but removes the chaos. - -Example: +## examples ```tsx import { define } from "forge" @@ -43,7 +38,7 @@ export const Button = define("button", { background: "blue", variants: { - kind: { + status: { danger: { background: "red" }, warning: { background: "yellow" }, } @@ -52,8 +47,8 @@ export const Button = define("button", { // Usage - - + + export const Profile = define("div", { padding: 50, @@ -90,3 +85,14 @@ import { Profile } from './whatever' ``` + +## see it + +Check out the `examples/` dir and view them at http://localhost:3300 by +cloning this repo and running the local web server: + +``` +bun install +bun dev +open http://localhost:3300 +``` diff --git a/examples/button.tsx b/examples/button.tsx index 322b287..5300a29 100644 --- a/examples/button.tsx +++ b/examples/button.tsx @@ -1,75 +1,88 @@ import { createScope } from '../src' -import { ExampleSection } from './ssr/helpers' +import { ExampleSection, theme } from './ssr/helpers' const { define } = createScope('Button') const Button = define('Root', { base: 'button', - padding: "12px 24px", + padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`, display: "inline-flex", alignItems: "center", justifyContent: "center", - gap: 8, - background: "#3b82f6", - color: "white", - border: "none", - borderRadius: 8, - fontSize: 16, - fontWeight: 600, + gap: theme.spacing.xs, + background: theme.colors.accent, + color: theme.colors.bg, + border: `1px solid ${theme.colors.accent}`, + borderRadius: theme.radius.sm, + fontSize: 14, + fontWeight: 400, cursor: "pointer", transition: "all 0.2s ease", userSelect: "none", - boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)", - transform: "translateY(0)", states: { ":not(:disabled):hover": { - transform: 'translateY(-2px)', - filter: 'brightness(1.05)' + background: theme.colors.accentDim, + borderColor: theme.colors.accentDim, }, ":not(:disabled):active": { transform: 'translateY(1px)', - boxShadow: '0 2px 3px rgba(0, 0, 0, 0.2)' }, }, variants: { intent: { primary: { - background: "#3b82f6", - color: "white", - boxShadow: "0 4px 6px rgba(59, 130, 246, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)", + background: theme.colors.accent, + color: theme.colors.bg, + border: `1px solid ${theme.colors.accent}`, }, secondary: { - background: "#f3f4f6", - color: "#374151", - boxShadow: "0 4px 6px rgba(0, 0, 0, 0.1), 0 2px 4px rgba(0, 0, 0, 0.06)", + background: theme.colors.bgElevated, + color: theme.colors.fg, + border: `1px solid ${theme.colors.border}`, + states: { + ":not(:disabled):hover": { + borderColor: theme.colors.borderActive, + } + } }, danger: { - background: "#ef4444", - color: "white", - boxShadow: "0 4px 6px rgba(239, 68, 68, 0.4), 0 2px 4px rgba(0, 0, 0, 0.1)", + background: "#ff0000", + color: theme.colors.bg, + border: "1px solid #ff0000", + states: { + ":not(:disabled):hover": { + background: "#cc0000", + borderColor: "#cc0000", + } + } }, ghost: { background: "transparent", - color: "#aaa", - boxShadow: "0 4px 6px rgba(0, 0, 0, 0.2), 0 2px 4px rgba(0, 0, 0, 0.1)", - border: "1px solid #eee", + color: theme.colors.fgMuted, + border: `1px solid ${theme.colors.border}`, + states: { + ":not(:disabled):hover": { + color: theme.colors.fg, + borderColor: theme.colors.borderActive, + } + } }, }, size: { small: { - padding: "8px 16px", - fontSize: 14, + padding: `${theme.spacing.xs}px ${theme.spacing.md}px`, + fontSize: 12, }, large: { - padding: "16px 32px", - fontSize: 18, + padding: `${theme.spacing.md}px ${theme.spacing.xl}px`, + fontSize: 16, }, }, disabled: { - opacity: 0.5, + opacity: 0.3, cursor: "not-allowed", }, }, @@ -77,7 +90,7 @@ const Button = define('Root', { const ButtonRow = define('Row', { display: 'flex', - gap: 16, + gap: theme.spacing.md, flexWrap: 'wrap', alignItems: 'center', }) diff --git a/examples/form.tsx b/examples/form.tsx index 7bf47e0..65f1873 100644 --- a/examples/form.tsx +++ b/examples/form.tsx @@ -1,15 +1,15 @@ import { define } from '../src' -import { ExampleSection } from './ssr/helpers' +import { ExampleSection, theme } from './ssr/helpers' const Input = define('Input', { base: 'input', - padding: '12px 16px', - fontSize: 16, - border: '2px solid #e5e7eb', - borderRadius: 8, - background: 'white', - color: '#111827', + padding: `${theme.spacing.sm}px ${theme.spacing.md}px`, + fontSize: 14, + border: `1px solid ${theme.colors.border}`, + borderRadius: theme.radius.sm, + background: theme.colors.bgElevated, + color: theme.colors.fg, transition: 'all 0.2s ease', width: '100%', boxSizing: 'border-box', @@ -17,12 +17,11 @@ const Input = define('Input', { states: { ':focus': { outline: 'none', - borderColor: '#3b82f6', - boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)' + borderColor: theme.colors.borderActive, }, ':disabled': { - background: '#f3f4f6', - color: '#9ca3af', + background: theme.colors.bg, + color: theme.colors.fgDim, cursor: 'not-allowed' } }, @@ -30,20 +29,18 @@ const Input = define('Input', { variants: { status: { error: { - borderColor: '#ef4444', + borderColor: '#ff0000', states: { ':focus': { - borderColor: '#ef4444', - boxShadow: '0 0 0 3px rgba(239, 68, 68, 0.1)' + borderColor: '#ff0000', } } }, success: { - borderColor: '#10b981', + borderColor: theme.colors.accent, states: { ':focus': { - borderColor: '#10b981', - boxShadow: '0 0 0 3px rgba(16, 185, 129, 0.1)' + borderColor: theme.colors.accent, } } } @@ -54,12 +51,12 @@ const Input = define('Input', { const Textarea = define('Textarea', { base: 'textarea', - padding: '12px 16px', - fontSize: 16, - border: '2px solid #e5e7eb', - borderRadius: 8, - background: 'white', - color: '#111827', + padding: `${theme.spacing.sm}px ${theme.spacing.md}px`, + fontSize: 14, + border: `1px solid ${theme.colors.border}`, + borderRadius: theme.radius.sm, + background: theme.colors.bgElevated, + color: theme.colors.fg, transition: 'all 0.2s ease', width: '100%', minHeight: 120, @@ -70,32 +67,31 @@ const Textarea = define('Textarea', { states: { ':focus': { outline: 'none', - borderColor: '#3b82f6', - boxShadow: '0 0 0 3px rgba(59, 130, 246, 0.1)' + borderColor: theme.colors.borderActive, } } }) const FormGroup = define('FormGroup', { - marginBottom: 24, + marginBottom: theme.spacing.lg, parts: { Label: { base: 'label', display: 'block', fontSize: 14, - fontWeight: 600, - color: '#374151', - marginBottom: 8 + fontWeight: 400, + color: theme.colors.fg, + marginBottom: theme.spacing.xs }, Helper: { - fontSize: 13, - color: '#6b7280', + fontSize: 12, + color: theme.colors.fgMuted, marginTop: 6 }, Error: { - fontSize: 13, - color: '#ef4444', + fontSize: 12, + color: '#ff0000', marginTop: 6 } }, @@ -116,29 +112,29 @@ const Checkbox = define('Checkbox', { parts: { Input: { base: 'input[type=checkbox]', - width: 20, - height: 20, + width: 18, + height: 18, cursor: 'pointer' }, Label: { base: 'label', display: 'flex', alignItems: 'center', - gap: 12, + gap: theme.spacing.sm, cursor: 'pointer', - fontSize: 16, - color: '#374151', + fontSize: 14, + color: theme.colors.fgMuted, states: { ':hover': { - color: '#111827' + color: theme.colors.fg } }, selectors: { '@Input:disabled + &': { cursor: 'not-allowed', - color: '#9ca3af' + color: theme.colors.fgDim } } } @@ -164,19 +160,20 @@ const FormExamples = define('FormExamples', { const Button = define('FormButton', { base: 'button', - padding: '12px 24px', - fontSize: 16, - fontWeight: 600, - border: 'none', - borderRadius: 8, + padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`, + fontSize: 14, + fontWeight: 400, + border: `1px solid ${theme.colors.accent}`, + borderRadius: theme.radius.sm, cursor: 'pointer', transition: 'all 0.2s ease', - background: '#3b82f6', - color: 'white', + background: theme.colors.accent, + color: theme.colors.bg, states: { ':hover': { - background: '#2563eb' + background: theme.colors.accentDim, + borderColor: theme.colors.accentDim, }, ':active': { transform: 'translateY(1px)' @@ -186,11 +183,12 @@ const Button = define('FormButton', { variants: { variant: { secondary: { - background: '#6b7280', - color: 'white', + background: theme.colors.bgElevated, + color: theme.colors.fg, + border: `1px solid ${theme.colors.border}`, states: { ':hover': { - background: '#4b5563' + borderColor: theme.colors.borderActive, } } } @@ -200,8 +198,8 @@ const Button = define('FormButton', { const ButtonGroup = define('FormButtonGroup', { display: 'flex', - gap: 12, - marginTop: 24 + gap: theme.spacing.sm, + marginTop: theme.spacing.lg }) export const FormExamplesContent = () => ( diff --git a/examples/navigation.tsx b/examples/navigation.tsx index 8bd7313..780bec3 100644 --- a/examples/navigation.tsx +++ b/examples/navigation.tsx @@ -1,5 +1,5 @@ import { define } from '../src' -import { ExampleSection } from './ssr/helpers' +import { ExampleSection, theme } from './ssr/helpers' const TabSwitcher = define('TabSwitcher', { parts: { @@ -10,41 +10,41 @@ const TabSwitcher = define('TabSwitcher', { TabBar: { display: 'flex', gap: 0, - borderBottom: '2px solid #e5e7eb', - marginBottom: 24, + borderBottom: `1px solid ${theme.colors.border}`, + marginBottom: theme.spacing.lg, }, TabLabel: { base: 'label', - padding: '12px 24px', + padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`, position: 'relative', - marginBottom: -2, + marginBottom: -1, background: 'transparent', - borderBottom: '2px solid transparent', - color: '#6b7280', + borderBottom: '1px solid transparent', + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - color: '#111827', + color: theme.colors.fg, } }, selectors: { '@Input:checked + &': { - color: '#3b82f6', - borderBottom: '2px solid #3b82f6' + color: theme.colors.accent, + borderBottom: `1px solid ${theme.colors.accent}` } } }, Content: { display: 'none', - padding: 20, - background: '#f9fafb', - borderRadius: 8, + padding: theme.spacing.lg, + background: theme.colors.bgElevated, + border: `1px solid ${theme.colors.border}`, + borderRadius: theme.radius.sm, selectors: { '@Input:checked ~ &': { @@ -91,36 +91,37 @@ const Pills = define('Pills', { }, PillBar: { display: 'flex', - gap: 8, + gap: theme.spacing.xs, flexWrap: 'wrap', }, PillLabel: { base: 'label', - padding: '8px 16px', - background: '#f3f4f6', - border: 'none', + padding: `${theme.spacing.xs}px ${theme.spacing.md}px`, + background: theme.colors.bgElevated, + border: `1px solid ${theme.colors.border}`, borderRadius: 20, - color: '#6b7280', + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - background: '#e5e7eb', - color: '#111827', + borderColor: theme.colors.borderActive, + color: theme.colors.fg, } }, selectors: { '@Input:checked + &': { - background: '#3b82f6', - color: 'white' + background: theme.colors.accent, + borderColor: theme.colors.accent, + color: theme.colors.bg }, '@Input:checked + &:hover': { - background: '#2563eb' + background: theme.colors.accentDim, + borderColor: theme.colors.accentDim, } } } @@ -164,34 +165,36 @@ const VerticalNav = define('VerticalNav', { NavLabel: { base: 'label', - padding: '12px 16px', + padding: `${theme.spacing.sm}px ${theme.spacing.md}px`, display: 'flex', alignItems: 'center', - gap: 12, + gap: theme.spacing.sm, background: 'transparent', - borderRadius: 8, - color: '#6b7280', + border: `1px solid transparent`, + borderRadius: theme.radius.sm, + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - background: '#f3f4f6', - color: '#111827', + background: theme.colors.bgElevated, + borderColor: theme.colors.border, + color: theme.colors.fg, } }, selectors: { '@Input:checked + &': { - background: '#eff6ff', - color: '#3b82f6', + background: theme.colors.bgElevated, + borderColor: theme.colors.accent, + color: theme.colors.accent, }, '@Input:checked + &:hover': { - background: '#dbeafe', - color: '#2563eb' + borderColor: theme.colors.accentDim, + color: theme.colors.accentDim } } }, @@ -201,7 +204,7 @@ const VerticalNav = define('VerticalNav', { display: 'flex', alignItems: 'center', justifyContent: 'center', - fontSize: 18, + fontSize: 16, } }, @@ -232,33 +235,32 @@ const VerticalNav = define('VerticalNav', { const Breadcrumbs = define('Breadcrumbs', { display: 'flex', alignItems: 'center', - gap: 8, + gap: theme.spacing.xs, flexWrap: 'wrap', parts: { Item: { base: 'a', - color: '#6b7280', + color: theme.colors.fgMuted, fontSize: 14, textDecoration: 'none', transition: 'color 0.2s ease', states: { ':hover': { - color: '#3b82f6', + color: theme.colors.accent, } } }, Separator: { - color: '#d1d5db', + color: theme.colors.fgDim, fontSize: 14, userSelect: 'none', }, Current: { - color: '#111827', + color: theme.colors.fg, fontSize: 14, - fontWeight: 500, } }, @@ -287,26 +289,25 @@ const Breadcrumbs = define('Breadcrumbs', { const Tabs = define('Tabs', { display: 'flex', gap: 0, - borderBottom: '2px solid #e5e7eb', + borderBottom: `1px solid ${theme.colors.border}`, parts: { Tab: { base: 'button', - padding: '12px 24px', + padding: `${theme.spacing.sm}px ${theme.spacing.lg}px`, position: 'relative', - marginBottom: -2, + marginBottom: -1, background: 'transparent', border: 'none', - borderBottom: '2px solid transparent', - color: '#6b7280', + borderBottom: '1px solid transparent', + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - color: '#111827', + color: theme.colors.fg, } } } @@ -316,8 +317,8 @@ const Tabs = define('Tabs', { active: { parts: { Tab: { - color: '#3b82f6', - borderBottom: '2px solid #3b82f6', + color: theme.colors.accent, + borderBottom: `1px solid ${theme.colors.accent}`, } } } @@ -338,26 +339,25 @@ const Tabs = define('Tabs', { const SimplePills = define('SimplePills', { display: 'flex', - gap: 8, + gap: theme.spacing.xs, flexWrap: 'wrap', parts: { Pill: { base: 'button', - padding: '8px 16px', - background: '#f3f4f6', - border: 'none', + padding: `${theme.spacing.xs}px ${theme.spacing.md}px`, + background: theme.colors.bgElevated, + border: `1px solid ${theme.colors.border}`, borderRadius: 20, - color: '#6b7280', + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - background: '#e5e7eb', - color: '#111827', + borderColor: theme.colors.borderActive, + color: theme.colors.fg, } } } @@ -367,12 +367,13 @@ const SimplePills = define('SimplePills', { active: { parts: { Pill: { - background: '#3b82f6', - color: 'white', + background: theme.colors.accent, + borderColor: theme.colors.accent, + color: theme.colors.bg, states: { ':hover': { - background: '#2563eb', - color: 'white', + background: theme.colors.accentDim, + borderColor: theme.colors.accentDim, } } } @@ -402,24 +403,24 @@ const SimpleVerticalNav = define('SimpleVerticalNav', { parts: { NavItem: { base: 'button', - padding: '12px 16px', + padding: `${theme.spacing.sm}px ${theme.spacing.md}px`, display: 'flex', alignItems: 'center', - gap: 12, + gap: theme.spacing.sm, background: 'transparent', - border: 'none', - borderRadius: 8, - color: '#6b7280', + border: `1px solid transparent`, + borderRadius: theme.radius.sm, + color: theme.colors.fgMuted, fontSize: 14, - fontWeight: 500, textAlign: 'left', cursor: 'pointer', transition: 'all 0.2s ease', states: { ':hover': { - background: '#f3f4f6', - color: '#111827', + background: theme.colors.bgElevated, + borderColor: theme.colors.border, + color: theme.colors.fg, } } }, @@ -429,7 +430,7 @@ const SimpleVerticalNav = define('SimpleVerticalNav', { display: 'flex', alignItems: 'center', justifyContent: 'center', - fontSize: 18, + fontSize: 16, } }, @@ -437,12 +438,13 @@ const SimpleVerticalNav = define('SimpleVerticalNav', { active: { parts: { NavItem: { - background: '#eff6ff', - color: '#3b82f6', + background: theme.colors.bgElevated, + borderColor: theme.colors.accent, + color: theme.colors.accent, states: { ':hover': { - background: '#dbeafe', - color: '#2563eb', + borderColor: theme.colors.accentDim, + color: theme.colors.accentDim, } } } diff --git a/examples/profile.tsx b/examples/profile.tsx index 0409113..0c8a3e0 100644 --- a/examples/profile.tsx +++ b/examples/profile.tsx @@ -1,22 +1,22 @@ import { define } from '../src' -import { ExampleSection } from './ssr/helpers' +import { ExampleSection, theme } from './ssr/helpers' const UserProfile = define('UserProfile', { base: 'div', - padding: 24, + padding: theme.spacing.lg, maxWidth: 600, margin: "0 auto", - background: "white", - borderRadius: 12, - boxShadow: "0 2px 8px rgba(0,0,0,0.1)", + background: theme.colors.bgElevated, + border: `1px solid ${theme.colors.border}`, + borderRadius: theme.radius.md, parts: { Header: { display: "flex", alignItems: "center", - gap: 16, - marginBottom: 16, + gap: theme.spacing.md, + marginBottom: theme.spacing.md, }, Avatar: { base: 'img', @@ -24,34 +24,34 @@ const UserProfile = define('UserProfile', { height: 64, borderRadius: "50%", objectFit: "cover", - border: "3px solid #e5e7eb", + border: `2px solid ${theme.colors.border}`, }, Info: { flex: 1, }, Name: { marginBottom: 4, - fontSize: 20, - fontWeight: 600, - color: "#111827", + fontSize: 18, + fontWeight: 400, + color: theme.colors.fg, }, Handle: { fontSize: 14, - color: "#6b7280", + color: theme.colors.fgMuted, }, Bio: { - marginBottom: 16, + marginBottom: theme.spacing.md, width: "100%", fontSize: 14, lineHeight: 1.6, - color: "#374151", + color: theme.colors.fgMuted, wordWrap: "break-word", }, Stats: { display: "flex", - gap: 24, - paddingTop: 16, - borderTop: "1px solid #e5e7eb", + gap: theme.spacing.lg, + paddingTop: theme.spacing.md, + borderTop: `1px solid ${theme.colors.border}`, }, Stat: { display: "flex", @@ -60,12 +60,12 @@ const UserProfile = define('UserProfile', { }, StatValue: { fontSize: 18, - fontWeight: 600, - color: "#111827", + fontWeight: 400, + color: theme.colors.fg, }, StatLabel: { fontSize: 12, - color: "#6b7280", + color: theme.colors.fgMuted, textTransform: "uppercase", }, }, @@ -114,19 +114,7 @@ const UserProfile = define('UserProfile', { verified: { parts: { Avatar: { - border: "3px solid #3b82f6", - }, - }, - }, - theme: { - dark: { - background: "#1f2937", - parts: { - Name: { color: "#f9fafb" }, - Handle: { color: "#9ca3af" }, - Bio: { color: "#d1d5db" }, - Stats: { borderTop: "1px solid #374151" }, - StatValue: { color: "#f9fafb" }, + border: `2px solid ${theme.colors.accent}`, }, }, }, @@ -207,10 +195,9 @@ export const ProfileExamplesContent = () => ( /> - + ( <> -

Explore component examples built with Forge - Client-side SPA version

+

Client-side rendered examples. Click around, check the source.

@@ -179,6 +179,20 @@ export function route(path: string) { } } +const HomeLink = define('HomeLink', { + base: 'a', + + color: theme.colors.fgMuted, + textDecoration: 'none', + fontSize: 14, + + states: { + hover: { + color: theme.colors.fg, + } + } +}) + export function App() { const path = window.location.pathname @@ -186,7 +200,7 @@ export function App() {