diff --git a/site/package.json b/site/package.json index aaed0a4d..232f5938 100644 --- a/site/package.json +++ b/site/package.json @@ -7,8 +7,9 @@ "build": "vite build", "preview": "vite preview", "check:ui": "node scripts/check-ui-language.mjs", + "check:styles": "node scripts/check-style-ownership.mjs", "test": "vitest run --config vitest.config.ts", - "typecheck": "npm run check:ui && tsc -p tsconfig.json --noEmit" + "typecheck": "npm run check:ui && npm run check:styles && tsc -p tsconfig.json --noEmit" }, "dependencies": { "@interactive-os/json-document-editing": "*", diff --git a/site/scripts/check-style-ownership.mjs b/site/scripts/check-style-ownership.mjs new file mode 100644 index 00000000..e9560f6b --- /dev/null +++ b/site/scripts/check-style-ownership.mjs @@ -0,0 +1,31 @@ +import { readFile } from "node:fs/promises"; + +const [css, recipes, tailwind] = await Promise.all([ + readFile(new URL("../src/app/index.css", import.meta.url), "utf8"), + readFile(new URL("../src/shared/ui/styles.ts", import.meta.url), "utf8"), + readFile(new URL("../tailwind.config.cjs", import.meta.url), "utf8"), +]); + +const violations = []; + +if (!css.includes("@layer tokens, base, components, utilities;")) { + violations.push("index.css must declare the cascade layer order."); +} + +for (const token of ["color-paper", "color-ink", "space-page-inline", "font-size-page-title"]) { + if (!css.includes(`--${token}:`)) violations.push(`index.css is missing semantic token --${token}.`); + if (!tailwind.includes(`--${token}`)) violations.push(`Tailwind does not consume semantic token --${token}.`); +} + +for (const routeNamespace of ["home", "workbench"]) { + if (new RegExp(`^ ${routeNamespace}:`, "m").test(recipes)) { + violations.push(`shared/ui/styles.ts contains route-owned namespace ${routeNamespace}.`); + } +} + +if (violations.length > 0) { + console.error(violations.join("\n")); + process.exitCode = 1; +} else { + console.log("Style ownership guard ok."); +} diff --git a/site/scripts/check-ui-language.mjs b/site/scripts/check-ui-language.mjs index c597d20d..cca6ffc0 100644 --- a/site/scripts/check-ui-language.mjs +++ b/site/scripts/check-ui-language.mjs @@ -31,7 +31,7 @@ const violations = sourceRoots.flatMap((root) => collect(path.join(siteRoot, roo .flatMap((file) => visualUtilityViolations(fs.readFileSync(file, "utf8"), path.relative(siteRoot, file))); if (violations.length > 0) { - console.error("UI language violations must move to src/shared/ui:"); + console.error("UI language violations must move to src/shared/ui or an explicit route-owned *-styles.ts module:"); violations.forEach((violation) => console.error(`- ${violation}`)); process.exitCode = 1; } else { @@ -43,6 +43,7 @@ function collect(directory) { const entryPath = path.join(directory, entry.name); if (entry.isDirectory()) return collect(entryPath); if (entry.name.endsWith(".gen.ts") || entry.name.endsWith(".gen.tsx")) return []; + if (entry.name.endsWith("-styles.ts")) return []; return /\.tsx?$/.test(entry.name) ? [entryPath] : []; }); } diff --git a/site/src/app/index.css b/site/src/app/index.css index 0b76b628..b1f221a7 100644 --- a/site/src/app/index.css +++ b/site/src/app/index.css @@ -1,3 +1,24 @@ +@layer tokens, base, components, utilities; + +@layer tokens { + :root { + --color-paper: 255 255 255; + --color-paper-warm: 251 248 242; + --color-ink: 69 67 62; + --color-ink-strong: 41 40 36; + --color-pencil: 119 115 107; + --color-pencil-light: 216 209 197; + --color-sage: 96 120 111; + --color-impact: 222 109 85; + --color-impact-ink: 159 73 55; + --color-impact-soft: 255 243 238; + --space-page-inline: 1rem; + --space-page-inline-wide: 2rem; + --font-size-page-title: 1.875rem; + --line-height-page-title: 2.25rem; + } +} + @tailwind base; @tailwind components; @tailwind utilities; diff --git a/site/src/routes/home/HomeRoute.tsx b/site/src/routes/home/HomeRoute.tsx index 6de6f7ef..f64d6a35 100644 --- a/site/src/routes/home/HomeRoute.tsx +++ b/site/src/routes/home/HomeRoute.tsx @@ -1,6 +1,7 @@ import { InlineCode } from "../../shared/ui/code-block"; import { ActionLink } from "../../shared/ui/interactive"; import { ui } from "../../shared/ui/styles"; +import { homeStyles } from "./home-styles"; const BASE_PATH = import.meta.env.BASE_URL.replace(/\/$/, ""); @@ -10,29 +11,29 @@ function sitePath(path: string): string { export function HomeRoute() { return ( -
-
-
-

+
+
+
+

json-document

-

+

One JSON document. Any editor.

-

+

A tiny headless API to read, query, patch, and subscribe.

-
- npm i @interactive-os/json-document +
+ npm i @interactive-os/json-document Get started
-
+
diff --git a/site/src/routes/home/home-styles.ts b/site/src/routes/home/home-styles.ts new file mode 100644 index 00000000..741b3e17 --- /dev/null +++ b/site/src/routes/home/home-styles.ts @@ -0,0 +1,13 @@ +export const homeStyles = { + page: "min-h-full overflow-hidden bg-paper", + hero: "relative isolate mx-auto min-h-[52rem] max-w-[92rem] overflow-hidden bg-paper lg:min-h-screen", + copy: "relative z-10 flex min-h-[52rem] max-w-2xl flex-col justify-start px-6 pb-[22rem] pt-20 sm:justify-center sm:px-12 sm:pb-[20rem] sm:pt-16 lg:min-h-screen lg:max-w-[42rem] lg:px-16 lg:pb-20 xl:px-24", + logoHeading: "m-0 w-full max-w-[29rem] text-ink-strong", + logo: "block h-auto w-full overflow-visible", + statement: "mb-0 mt-8 text-2xl font-semibold leading-tight tracking-[-0.035em] text-ink-strong sm:text-3xl", + description: "mb-0 mt-4 max-w-md text-base leading-7 text-ink", + entry: "mt-8 flex flex-wrap items-center gap-x-5 gap-y-3", + install: "font-mono text-xs leading-5 text-pencil", + artwork: "pointer-events-none absolute inset-0 z-0 m-0 overflow-hidden", + artworkImage: "absolute bottom-0 right-0 h-auto w-[140vw] max-w-none translate-x-[18%] sm:w-[112vw] sm:translate-x-[14%] lg:w-[78vw] lg:translate-x-[8%]", +} as const; diff --git a/site/src/routes/rich-text-demo/RichTextDemoRoute.tsx b/site/src/routes/rich-text-demo/RichTextDemoRoute.tsx index 2320e117..7c7e17ea 100644 --- a/site/src/routes/rich-text-demo/RichTextDemoRoute.tsx +++ b/site/src/routes/rich-text-demo/RichTextDemoRoute.tsx @@ -14,6 +14,7 @@ import { JsonInspector } from "../../shared/ui/json-inspector"; import { ActionButton } from "../../shared/ui/interactive"; import { PageFrame, PageHeader } from "../../shared/ui/primitives"; import { classes, ui } from "../../shared/ui/styles"; +import { richTextStyles } from "./rich-text-styles"; const initialDocument: RichTextDocument = { profile: "urn:interactive-os:json-document:rich-text:1", @@ -207,7 +208,7 @@ export function RichTextDemoRoute() { spellCheck={false} aria-label="Rich Text 편집기" data-testid="rich-text-editor" - className={classes(ui.workbench.richTextEditor, ui.state.focus)} + className={classes(richTextStyles.editor, ui.state.focus)} />

입력·삭제, Enter block split, IME composition, DOM Selection 복원, structured/HTML/plain Clipboard와 undo/redo가 모두 Official Rich Text intent 경로에 연결됩니다. diff --git a/site/src/routes/rich-text-demo/rich-text-styles.ts b/site/src/routes/rich-text-demo/rich-text-styles.ts new file mode 100644 index 00000000..25da0a34 --- /dev/null +++ b/site/src/routes/rich-text-demo/rich-text-styles.ts @@ -0,0 +1,3 @@ +export const richTextStyles = { + editor: "min-h-56 rounded-[6px] border border-pencil-light bg-paper p-5 text-base leading-7 text-ink-strong outline-none", +} as const; diff --git a/site/src/shared/ui/styles.ts b/site/src/shared/ui/styles.ts index 97462a06..bcdd62cf 100644 --- a/site/src/shared/ui/styles.ts +++ b/site/src/shared/ui/styles.ts @@ -1,7 +1,9 @@ +// Public recipes for shared UI primitives and patterns only. +// Page- or route-specific composition belongs beside its owning route. export const ui = { frame: { app: "bg-paper font-sans text-ink antialiased", - page: "min-h-full bg-paper px-4 pb-8 pt-4 lg:px-8", + page: "min-h-full bg-paper px-page pb-8 pt-4 lg:px-page-wide", content: "mx-auto max-w-6xl", header: "border-b border-pencil-light/60", hero: "border-b border-pencil-light/60 bg-transparent", @@ -35,7 +37,7 @@ export const ui = { }, text: { label: "text-xs font-medium text-ink", - title: "text-3xl font-semibold tracking-[-0.025em] text-ink-strong", + title: "text-page-title font-semibold tracking-[-0.025em] text-ink-strong", heading: "text-base font-semibold text-ink-strong", body: "text-sm leading-6 text-ink", meta: "text-xs leading-5 text-pencil", @@ -131,29 +133,6 @@ export const ui = { disabled: "disabled:cursor-not-allowed disabled:text-pencil", error: "text-impact-ink", }, - workbench: { - exampleShell: "overflow-hidden rounded-[10px] border border-pencil-light bg-paper shadow-[0_1px_2px_rgba(69,67,62,0.04),0_12px_32px_rgba(69,67,62,0.06)]", - exampleScenario: "flex items-center gap-3 border-b border-pencil-light bg-paper px-4 py-3", - exampleMarker: "h-8 w-1 rounded-full bg-impact", - exampleInspectors: "grid gap-3 border-t border-pencil-light bg-paper-warm p-3 lg:grid-cols-3", - darkStrip: "rounded-[10px] border border-ink bg-ink-strong text-paper", - darkLabel: "text-xs font-medium uppercase tracking-wide text-impact", - darkMeta: "text-[10px] text-pencil-light", - darkChip: "rounded-full border border-pencil text-xs text-paper-warm", - tab: "rounded-[6px] border border-transparent text-sm text-ink aria-pressed:border-impact aria-pressed:bg-paper-warm aria-pressed:text-ink-strong", - compactList: "rounded-[10px] border border-pencil-light bg-paper divide-y divide-pencil-light/70", - sidebar: "rounded-[6px] border border-pencil-light bg-paper-warm lg:border-l lg:border-t-0", - item: "rounded-[6px] border border-pencil-light bg-paper text-sm text-ink aria-pressed:border-impact aria-pressed:bg-paper-warm aria-pressed:ring-1 aria-pressed:ring-impact", - itemStrong: "rounded-[6px] border border-pencil-light bg-paper text-sm text-ink aria-pressed:border-impact aria-pressed:bg-paper-warm aria-pressed:ring-2 aria-pressed:ring-impact", - inspectorToggle: "bg-ink-strong text-xs font-medium text-paper", - inspectorGrid: "bg-ink", - inspectorCell: "bg-ink-strong text-paper-warm", - inspectorLabel: "text-[9px] font-medium uppercase tracking-wider text-impact", - canvas: "rounded-[10px] border border-pencil-light bg-paper-grid bg-[size:16px_16px]", - canvasObject: "rounded-[6px] border-2 border-paper text-xs font-medium text-paper shadow-[0_1px_2px_rgba(69,67,62,0.12)] outline-none aria-pressed:ring-2 aria-pressed:ring-impact aria-pressed:ring-offset-2", - marquee: "border border-impact bg-impact/10", - richTextEditor: "min-h-56 rounded-[6px] border border-pencil-light bg-paper p-5 text-base leading-7 text-ink-strong outline-none", - }, database: { toolbar: "rounded-[10px] border border-pencil-light bg-paper shadow-[0_1px_2px_rgba(69,67,62,0.04)]", table: "border-collapse bg-paper text-left text-sm", @@ -165,19 +144,6 @@ export const ui = { checkbox: "h-4 w-4 accent-impact", lease: "rounded-[6px] border border-impact/40 bg-impact-soft px-2 py-1 text-xs text-impact-ink", }, - home: { - page: "min-h-full overflow-hidden bg-paper", - hero: "relative isolate mx-auto min-h-[52rem] max-w-[92rem] overflow-hidden bg-paper lg:min-h-screen", - copy: "relative z-10 flex min-h-[52rem] max-w-2xl flex-col justify-start px-6 pb-[22rem] pt-20 sm:justify-center sm:px-12 sm:pb-[20rem] sm:pt-16 lg:min-h-screen lg:max-w-[42rem] lg:px-16 lg:pb-20 xl:px-24", - logoHeading: "m-0 w-full max-w-[29rem] text-ink-strong", - logo: "block h-auto w-full overflow-visible", - statement: "mb-0 mt-8 text-2xl font-semibold leading-tight tracking-[-0.035em] text-ink-strong sm:text-3xl", - description: "mb-0 mt-4 max-w-md text-base leading-7 text-ink", - entry: "mt-8 flex flex-wrap items-center gap-x-5 gap-y-3", - install: "font-mono text-xs leading-5 text-pencil", - artwork: "pointer-events-none absolute inset-0 z-0 m-0 overflow-hidden", - artworkImage: "absolute bottom-0 right-0 h-auto w-[140vw] max-w-none translate-x-[18%] sm:w-[112vw] sm:translate-x-[14%] lg:w-[78vw] lg:translate-x-[8%]", - }, } as const; export function classes(...values: ReadonlyArray): string { diff --git a/site/tailwind.config.cjs b/site/tailwind.config.cjs index f07a685b..08733e57 100644 --- a/site/tailwind.config.cjs +++ b/site/tailwind.config.cjs @@ -8,26 +8,33 @@ module.exports = { extend: { colors: { paper: { - DEFAULT: "#ffffff", - warm: "#fbf8f2", + DEFAULT: "rgb(var(--color-paper) / )", + warm: "rgb(var(--color-paper-warm) / )", }, ink: { - DEFAULT: "#45433e", - strong: "#292824", + DEFAULT: "rgb(var(--color-ink) / )", + strong: "rgb(var(--color-ink-strong) / )", }, pencil: { - DEFAULT: "#77736b", - light: "#d8d1c5", + DEFAULT: "rgb(var(--color-pencil) / )", + light: "rgb(var(--color-pencil-light) / )", }, - sage: "#60786f", + sage: "rgb(var(--color-sage) / )", impact: { - DEFAULT: "#de6d55", - ink: "#9f4937", - soft: "#fff3ee", + DEFAULT: "rgb(var(--color-impact) / )", + ink: "rgb(var(--color-impact-ink) / )", + soft: "rgb(var(--color-impact-soft) / )", }, }, + spacing: { + page: "var(--space-page-inline)", + "page-wide": "var(--space-page-inline-wide)", + }, + fontSize: { + "page-title": ["var(--font-size-page-title)", "var(--line-height-page-title)"], + }, backgroundImage: { - "paper-grid": "linear-gradient(#d8d1c5 1px, transparent 1px), linear-gradient(90deg, #d8d1c5 1px, transparent 1px)", + "paper-grid": "linear-gradient(rgb(var(--color-pencil-light)) 1px, transparent 1px), linear-gradient(90deg, rgb(var(--color-pencil-light)) 1px, transparent 1px)", }, }, },