Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "*",
Expand Down
31 changes: 31 additions & 0 deletions site/scripts/check-style-ownership.mjs
Original file line number Diff line number Diff line change
@@ -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.");
}
3 changes: 2 additions & 1 deletion site/scripts/check-ui-language.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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] : [];
});
}
21 changes: 21 additions & 0 deletions site/src/app/index.css
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
23 changes: 12 additions & 11 deletions site/src/routes/home/HomeRoute.tsx
Original file line number Diff line number Diff line change
@@ -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(/\/$/, "");

Expand All @@ -10,29 +11,29 @@ function sitePath(path: string): string {

export function HomeRoute() {
return (
<main className={ui.home.page}>
<section className={ui.home.hero} aria-labelledby="home-title">
<div className={ui.home.copy}>
<h1 id="home-title" className={ui.home.logoHeading}>
<main className={homeStyles.page}>
<section className={homeStyles.hero} aria-labelledby="home-title">
<div className={homeStyles.copy}>
<h1 id="home-title" className={homeStyles.logoHeading}>
<span className="sr-only">json-document</span>
<HomeWordmark />
</h1>
<p className={ui.home.statement}>
<p className={homeStyles.statement}>
One JSON document. Any editor.
</p>
<p className={ui.home.description}>
<p className={homeStyles.description}>
A tiny headless API to read, query, patch, and subscribe.
</p>

<div className={ui.home.entry}>
<InlineCode className={ui.home.install} prompt>npm i @interactive-os/json-document</InlineCode>
<div className={homeStyles.entry}>
<InlineCode className={homeStyles.install} prompt>npm i @interactive-os/json-document</InlineCode>
<ActionLink to="/docs/tutorial" kind="prominent">Get started</ActionLink>
</div>
</div>

<figure className={ui.home.artwork}>
<figure className={homeStyles.artwork}>
<img
className={ui.home.artworkImage}
className={homeStyles.artworkImage}
src={sitePath("/cat-enter.png")}
alt="A small cat struggling to press an oversized Enter key."
width="1200"
Expand All @@ -48,7 +49,7 @@ function HomeWordmark() {
return (
<svg
aria-hidden="true"
className={ui.home.logo}
className={homeStyles.logo}
viewBox="0 0 435 66"
xmlns="http://www.w3.org/2000/svg"
>
Expand Down
13 changes: 13 additions & 0 deletions site/src/routes/home/home-styles.ts
Original file line number Diff line number Diff line change
@@ -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;
3 changes: 2 additions & 1 deletion site/src/routes/rich-text-demo/RichTextDemoRoute.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)}
/>
<p className={classes("mb-0 mt-3", ui.text.meta)}>
입력·삭제, Enter block split, IME composition, DOM Selection 복원, structured/HTML/plain Clipboard와 undo/redo가 모두 Official Rich Text intent 경로에 연결됩니다.
Expand Down
3 changes: 3 additions & 0 deletions site/src/routes/rich-text-demo/rich-text-styles.ts
Original file line number Diff line number Diff line change
@@ -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;
42 changes: 4 additions & 38 deletions site/src/shared/ui/styles.ts
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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 | false | null | undefined>): string {
Expand Down
29 changes: 18 additions & 11 deletions site/tailwind.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,26 +8,33 @@ module.exports = {
extend: {
colors: {
paper: {
DEFAULT: "#ffffff",
warm: "#fbf8f2",
DEFAULT: "rgb(var(--color-paper) / <alpha-value>)",
warm: "rgb(var(--color-paper-warm) / <alpha-value>)",
},
ink: {
DEFAULT: "#45433e",
strong: "#292824",
DEFAULT: "rgb(var(--color-ink) / <alpha-value>)",
strong: "rgb(var(--color-ink-strong) / <alpha-value>)",
},
pencil: {
DEFAULT: "#77736b",
light: "#d8d1c5",
DEFAULT: "rgb(var(--color-pencil) / <alpha-value>)",
light: "rgb(var(--color-pencil-light) / <alpha-value>)",
},
sage: "#60786f",
sage: "rgb(var(--color-sage) / <alpha-value>)",
impact: {
DEFAULT: "#de6d55",
ink: "#9f4937",
soft: "#fff3ee",
DEFAULT: "rgb(var(--color-impact) / <alpha-value>)",
ink: "rgb(var(--color-impact-ink) / <alpha-value>)",
soft: "rgb(var(--color-impact-soft) / <alpha-value>)",
},
},
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)",
},
},
},
Expand Down