diff --git a/biome.json b/biome.json index da481c4..f572cef 100644 --- a/biome.json +++ b/biome.json @@ -14,6 +14,11 @@ "indentStyle": "space", "indentWidth": 2 }, + "css": { + "parser": { + "tailwindDirectives": true + } + }, "linter": { "enabled": true, "rules": { diff --git a/packages/frontend/README.md b/packages/frontend/README.md index 5c4780a..3f4e4d7 100644 --- a/packages/frontend/README.md +++ b/packages/frontend/README.md @@ -82,6 +82,10 @@ Make sure to deploy the output of `npm run build` This template comes with [Tailwind CSS](https://tailwindcss.com/) already configured for a simple default starting experience. You can use whatever CSS framework you prefer. +FoR-specific design token operation rules live at: + +- `app/design-tokens.md` + --- Built with ❤️ using React Router. diff --git a/packages/frontend/app/app.css b/packages/frontend/app/app.css index 99345d8..424a49a 100644 --- a/packages/frontend/app/app.css +++ b/packages/frontend/app/app.css @@ -1,15 +1,279 @@ @import "tailwindcss"; -@theme { - --font-sans: "Inter", ui-sans-serif, system-ui, sans-serif, - "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +:root { + color-scheme: light; + + /* Semantic Color: Text */ + --for-text-default: rgb(0 0 0 / 0.8); + --for-text-subtle: rgb(0 0 0 / 0.6); + --for-text-hint: rgb(0 0 0 / 0.3); + --for-text-invert: #ffffff; + --for-text-danger-default: #eb3d3d; + --for-text-danger-subtle: #ff6161; + --for-text-danger-strong: #8b1d1d; + + /* Semantic Color: Background */ + --for-bg-default: #ffffff; + --for-bg-subtle: #f2f3f0; + --for-bg-danger-default: #feebeb; + --for-bg-alpha-white-30: rgb(255 255 255 / 0.3); + --for-bg-alpha-white-60: rgb(255 255 255 / 0.6); + --for-bg-alpha-black-10: rgb(0 0 0 / 0.1); + --for-bg-modal: rgb(0 0 0 / 0.5); + + /* Semantic Color: Stroke */ + --for-stroke-default: rgb(0 0 0 / 0.05); + + /* Semantic Color: Button */ + --for-button-primary-frame-default: #454545; + --for-button-primary-frame-hover: #1a1a1a; + --for-button-primary-frame-disabled: #b4b4b4; + --for-button-primary-text-invert: #ffffff; + --for-button-primary-text-disabled: #858585; + + --for-button-secondary-frame-default: #ffffff; + --for-button-secondary-frame-hover: #1a1a1a; + --for-button-secondary-frame-disabled: #f0f0f0; + --for-button-secondary-stroke-default: #454545; + --for-button-secondary-stroke-hover: #1a1a1a; + --for-button-secondary-stroke-disabled: #b4b4b4; + --for-button-secondary-text-default: #454545; + --for-button-secondary-text-hover: #1a1a1a; + --for-button-secondary-text-disabled: #b4b4b4; + + /* Tertiary values are taken from screenshot labels; hover frame was not shown */ + --for-button-tertiary-frame-default: #757b7f; + --for-button-tertiary-frame-disabled: #aeaeae; + --for-button-tertiary-text-default: #e0e0e0; + --for-button-tertiary-text-hover: #ffffff; + --for-button-tertiary-text-disabled: #cfcfcf; + + --for-button-danger-frame-default: #eb3d3d; + --for-button-danger-frame-hover: #8b1d1d; + --for-button-danger-frame-pressed: #8b1d1d; + --for-button-danger-frame-disabled: #fbbdbf; + --for-button-danger-text-default: #eb3d3d; + --for-button-danger-text-hover: #8b1d1d; + --for-button-danger-text-pressed: #8b1d1d; + --for-button-danger-text-disabled: #fbbdbf; + --for-button-danger-text-invert: #ffffff; + + /* Semantic Color: Visual Accent */ + --for-visual-natural-1: #e4edf7; + --for-visual-natural-2: #d1dce8; + --for-visual-natural-3: #9fb2c4; + --for-visual-natural-4: #72818f; + --for-visual-natural-5: #414a52; + --for-visual-green-1: #a5fee1; + --for-visual-green-2: #3ff4b7; + --for-visual-green-3: #00e998; + --for-visual-green-4: #0ecb8c; + --for-visual-green-5: #009e69; + --for-visual-red-1: #ffdadd; + --for-visual-red-2: #ff7885; + --for-visual-red-3: #eb3d3d; + --for-visual-red-4: #9b2727; + --for-visual-red-5: #5c1414; + + /* Semantic spacing tokens (source of truth) */ + --for-space-0: 0px; + --for-space-2: 2px; + --for-space-4: 4px; + --for-space-6: 6px; + --for-space-8: 8px; + --for-space-12: 12px; + --for-space-16: 16px; + --for-space-20: 20px; + --for-space-24: 24px; + --for-space-28: 28px; + --for-space-32: 32px; + --for-space-40: 40px; + + /* shadcn/ui compatible tokens mapped from FoR semantics */ + --background: var(--for-bg-subtle); + --foreground: var(--for-text-default); + --card: var(--for-bg-default); + --card-foreground: var(--for-text-default); + --popover: var(--for-bg-default); + --popover-foreground: var(--for-text-default); + --primary: var(--for-button-primary-frame-default); + --primary-foreground: var(--for-button-primary-text-invert); + --secondary: var(--for-button-secondary-frame-default); + --secondary-foreground: var(--for-button-secondary-text-default); + --muted: var(--for-bg-subtle); + --muted-foreground: var(--for-text-subtle); + --accent: var(--for-visual-green-3); + --accent-foreground: var(--for-text-default); + --destructive: var(--for-button-danger-frame-default); + --destructive-foreground: var(--for-button-danger-text-invert); + --border: var(--for-stroke-default); + --input: var(--for-stroke-default); + --ring: var(--for-button-primary-frame-hover); + --radius: 12px; +} + +@theme inline { + /* Typography */ + --font-ui: + "Zen Kaku Gothic New", "Roboto", ui-sans-serif, system-ui, sans-serif; + --font-latin: "Roboto", ui-sans-serif, system-ui, sans-serif; + --font-sans: var(--font-ui); + + --text-ui-10: 10px; + --text-ui-10--line-height: 1.3; + --text-ui-12: 12px; + --text-ui-12--line-height: 1.3; + --text-ui-13: 13px; + --text-ui-13--line-height: 1.3; + --text-ui-16: 16px; + --text-ui-16--line-height: 1.3; + --text-ui-20: 20px; + --text-ui-20--line-height: 1.3; + + --text-content-display-l: 28px; + --text-content-display-l--line-height: 1.3; + --text-content-display-m: 24px; + --text-content-display-m--line-height: 1.3; + --text-content-display-s: 20px; + --text-content-display-s--line-height: 1.3; + + --text-content-headline-l: 20px; + --text-content-headline-l--line-height: 1.3; + --text-content-headline-m: 17px; + --text-content-headline-m--line-height: 1.3; + --text-content-headline-s: 16px; + --text-content-headline-s--line-height: 1.3; + + --text-content-number-l: 32px; + --text-content-number-l--line-height: 1.3; + --text-content-number-m: 24px; + --text-content-number-m--line-height: 1.3; + --text-content-number-s: 16px; + --text-content-number-s--line-height: 1.3; + + --text-content-body-l: 15px; + --text-content-body-l--line-height: 1.6; + --text-content-body-m: 14px; + --text-content-body-m--line-height: 1.6; + --text-content-body-s: 13px; + --text-content-body-s--line-height: 1.6; + + --text-content-caption: 12px; + --text-content-caption--line-height: 1.6; + + /* shadcn/ui color namespace */ + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + + --color-text-danger-default: var(--for-text-danger-default); + --color-text-danger-subtle: var(--for-text-danger-subtle); + --color-text-danger-strong: var(--for-text-danger-strong); + + --color-button-primary-frame: var(--for-button-primary-frame-default); + --color-button-primary-frame-hover: var(--for-button-primary-frame-hover); + --color-button-primary-frame-disabled: var( + --for-button-primary-frame-disabled + ); + --color-button-primary-text: var(--for-button-primary-text-invert); + --color-button-primary-text-disabled: var(--for-button-primary-text-disabled); + + --color-button-secondary-frame: var(--for-button-secondary-frame-default); + --color-button-secondary-frame-hover: var(--for-button-secondary-frame-hover); + --color-button-secondary-frame-disabled: var( + --for-button-secondary-frame-disabled + ); + --color-button-secondary-stroke: var(--for-button-secondary-stroke-default); + --color-button-secondary-stroke-hover: var( + --for-button-secondary-stroke-hover + ); + --color-button-secondary-stroke-disabled: var( + --for-button-secondary-stroke-disabled + ); + --color-button-secondary-text: var(--for-button-secondary-text-default); + --color-button-secondary-text-hover: var(--for-button-secondary-text-hover); + --color-button-secondary-text-disabled: var( + --for-button-secondary-text-disabled + ); + + --color-button-tertiary-frame: var(--for-button-tertiary-frame-default); + --color-button-tertiary-frame-disabled: var( + --for-button-tertiary-frame-disabled + ); + --color-button-tertiary-text: var(--for-button-tertiary-text-default); + --color-button-tertiary-text-hover: var(--for-button-tertiary-text-hover); + --color-button-tertiary-text-disabled: var( + --for-button-tertiary-text-disabled + ); + + --color-button-danger-frame: var(--for-button-danger-frame-default); + --color-button-danger-frame-hover: var(--for-button-danger-frame-hover); + --color-button-danger-frame-pressed: var(--for-button-danger-frame-pressed); + --color-button-danger-frame-disabled: var(--for-button-danger-frame-disabled); + --color-button-danger-text: var(--for-button-danger-text-default); + --color-button-danger-text-hover: var(--for-button-danger-text-hover); + --color-button-danger-text-pressed: var(--for-button-danger-text-pressed); + --color-button-danger-text-disabled: var(--for-button-danger-text-disabled); + --color-button-danger-text-invert: var(--for-button-danger-text-invert); + + --color-visual-natural-1: var(--for-visual-natural-1); + --color-visual-natural-2: var(--for-visual-natural-2); + --color-visual-natural-3: var(--for-visual-natural-3); + --color-visual-natural-4: var(--for-visual-natural-4); + --color-visual-natural-5: var(--for-visual-natural-5); + --color-visual-green-1: var(--for-visual-green-1); + --color-visual-green-2: var(--for-visual-green-2); + --color-visual-green-3: var(--for-visual-green-3); + --color-visual-green-4: var(--for-visual-green-4); + --color-visual-green-5: var(--for-visual-green-5); + --color-visual-red-1: var(--for-visual-red-1); + --color-visual-red-2: var(--for-visual-red-2); + --color-visual-red-3: var(--for-visual-red-3); + --color-visual-red-4: var(--for-visual-red-4); + --color-visual-red-5: var(--for-visual-red-5); + + /* Radius */ + --radius-sm: calc(var(--radius) - 6px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + + /* + Spacing operation rule: + - Tailwind spacing utilities are px-based via --spacing: 1px. + - Use only 0,2,4,6,8,12,16,20,24,28,32,40. + - Disallow arbitrary spacing values like p-[14px], m-[3px], gap-[22px]. + */ + --spacing: 1px; } -html, -body { - @apply bg-white dark:bg-gray-950; +@layer base { + * { + @apply border-border; + } + + html, + body { + min-height: 100%; + } - @media (prefers-color-scheme: dark) { - color-scheme: dark; + body { + @apply bg-background text-foreground font-ui text-ui-13 antialiased; } } diff --git a/packages/frontend/app/components/ui/button.tsx b/packages/frontend/app/components/ui/button.tsx new file mode 100644 index 0000000..2a4c53c --- /dev/null +++ b/packages/frontend/app/components/ui/button.tsx @@ -0,0 +1,58 @@ +import * as React from "react"; + +import { cn } from "~/lib/utils"; + +const buttonVariantClasses = { + default: + "border-transparent bg-button-primary-frame text-button-primary-text hover:bg-button-primary-frame-hover active:bg-button-primary-frame-hover disabled:bg-button-primary-frame-disabled disabled:text-button-primary-text-disabled", + secondary: + "border-button-secondary-stroke bg-button-secondary-frame text-button-secondary-text hover:border-button-secondary-stroke-hover hover:bg-button-secondary-frame-hover hover:text-button-secondary-text-hover active:border-button-secondary-stroke-hover active:bg-button-secondary-frame-hover active:text-button-secondary-text-hover disabled:border-button-secondary-stroke-disabled disabled:bg-button-secondary-frame-disabled disabled:text-button-secondary-text-disabled", + ghost: + "border-transparent bg-button-tertiary-frame text-button-tertiary-text hover:text-button-tertiary-text-hover active:text-button-tertiary-text-hover disabled:bg-button-tertiary-frame-disabled disabled:text-button-tertiary-text-disabled", + destructive: + "border-transparent bg-button-danger-frame text-button-danger-text-invert hover:bg-button-danger-frame-hover active:bg-button-danger-frame-pressed disabled:bg-button-danger-frame-disabled disabled:text-button-danger-text-disabled", +} as const; + +const buttonSizeClasses = { + default: "h-40 px-16", + sm: "h-32 px-12 text-ui-10", + lg: "h-40 px-20 text-ui-16", + icon: "size-40 p-0", +} as const; + +type ButtonVariant = keyof typeof buttonVariantClasses; +type ButtonSize = keyof typeof buttonSizeClasses; + +export interface ButtonProps + extends React.ButtonHTMLAttributes { + variant?: ButtonVariant; + size?: ButtonSize; +} + +export const Button = React.forwardRef( + ( + { + className, + variant = "default", + size = "default", + type = "button", + ...props + }, + ref, + ) => { + return ( + + + + + + + + +
+

+ Semantic Color Mapping +

+

+ Reference rule: component styles use shadcn tokens such as{" "} + bg-background, text-foreground, and{" "} + border-border. Raw FoR variables remain source-only. +

+
+ + + + + + + + + + +
+
+ +
+

+ Typography Utilities +

+

+ font-ui uses Zen Kaku Gothic New + Roboto, and every{" "} + text-ui-* token enforces line-height 130%. Content styles + are exposed as text-content-*. +

+
+ {uiTypographyRows.map(({ className, label }) => ( +

+ {label} / line-height 130% +

+ ))} +
+
+ {contentTypographyRows.map(({ className, label }) => ( +

+ {label}

- - + ))}
- +
+ +
+

+ Spacing Scale Rule +

+

+ Allowed spacing values are 0, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32, and + 40px. Avoid arbitrary spacing classes such as p-[14px]. +

+
+ {spacingScale.map((space) => ( +
+
+ + {space}px + +
+ ))} +
+
); } -const resources = [ +function ColorChip({ + label, + swatchClass, +}: { + label: string; + swatchClass: string; +}) { + return ( +
+ + {label} +
+ ); +} + +const typographyRows = [ + { className: "text-ui-10", label: "UI 10" }, + { className: "text-ui-12", label: "UI 12" }, + { className: "text-ui-13", label: "UI 13" }, + { className: "text-ui-16", label: "UI 16" }, + { className: "text-ui-20", label: "UI 20" }, +]; + +const uiTypographyRows = typographyRows; + +const contentTypographyRows = [ + { + className: "text-content-display-l", + label: "Display/L 28px 130%", + }, + { + className: "text-content-display-m", + label: "Display/M 24px 130%", + }, + { + className: "text-content-display-s", + label: "Display/S 20px 130%", + }, + { + className: "text-content-headline-l", + label: "Headline/L 20px 130%", + }, + { + className: "text-content-headline-m", + label: "Headline/M 17px 130%", + }, + { + className: "text-content-body-l", + label: "Body/L 15px 160%", + }, + { + className: "text-content-body-m", + label: "Body/M 14px 160%", + }, { - href: "https://reactrouter.com/docs", - text: "React Router Docs", - icon: ( - - - - ), + className: "text-content-body-s", + label: "Body/S 13px 160%", }, { - href: "https://rmx.as/discord", - text: "Join Discord", - icon: ( - - - - ), + className: "text-content-caption", + label: "Caption 12px 160%", }, ]; + +const spacingScale = [0, 2, 4, 6, 8, 12, 16, 20, 24, 28, 32, 40]; diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 46c0886..15a65ca 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -14,6 +14,7 @@ "@react-router/node": "^7.9.2", "@react-router/serve": "^7.9.2", "isbot": "^5.1.31", + "lucide-react": "^0.554.0", "permissionless": "^0.2.57", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9b0a687..b830b7d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: isbot: specifier: ^5.1.31 version: 5.1.32 + lucide-react: + specifier: ^0.554.0 + version: 0.554.0(react@19.2.0) permissionless: specifier: ^0.2.57 version: 0.2.57(viem@2.43.5(bufferutil@4.1.0)(typescript@5.9.3)(utf-8-validate@5.0.10)(zod@3.25.76))