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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ The owner talks to you in English.
| `packages/vocab` | Shared vocabularies, filters, extra-field definitions, daylight math: anything both the Worker and a client must agree on |
| `packages/fov-math` | Camera/lens/FOV/overlay/reframe math |
| `packages/ui` | The web design system (`@openclapper/ui`): components, tokens and component CSS, theme switch; used by the Studio and the admin board |
| `packages/design` + `apps/specimen` | The rewrite's design system (`@openclapper/design`, `docs/rewrite/`) and its specimen app (`make specimen`); not used by the current apps, never on the same page as `@openclapper/ui` |
| `packages/service-kit` | What both Workers share: `http.ts` router + `assert*` helpers, Access verification, password hashing |

## Core model in one paragraph
Expand Down
5 changes: 4 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# The local development stack: thin wrappers around the pnpm scripts in package.json, which stay the
# source of truth. Everything runs in the foreground; stop a server with Ctrl-C. Deploying is not
# here: it runs from a deployment repository (docs/self-hosting.md).
.PHONY: help init check all all-down all-restart dev worker studio admin mobile dev-build apk-local
.PHONY: help init check all all-down all-restart dev worker studio admin specimen mobile dev-build apk-local

help: ## Show this list
@grep -hE '^[a-z-]+:.*?## ' $(MAKEFILE_LIST) \
Expand Down Expand Up @@ -34,6 +34,9 @@ studio: ## Studio only -> http://localhost:5173 (needs `make worker` for /api)
admin: ## Admin board -> http://localhost:5174 with its Worker on :8788 (local D1, no Access: admin@localhost)
pnpm dev:admin

specimen: ## Design system specimens (rewrite, packages/design) -> http://localhost:5175, no Worker needed
pnpm -C apps/specimen dev

mobile: ## Local Worker behind a quick tunnel + Metro for the dev build (OpenClapper Dev); LOCAL data
pnpm dev:mobile

Expand Down
13 changes: 13 additions & 0 deletions apps/specimen/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenClapper design specimen</title>
<link rel="icon" href="/favicon.png">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
20 changes: 20 additions & 0 deletions apps/specimen/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"name": "@openclapper/specimen",
"private": true,
"version": "0.0.0",
"license": "Apache-2.0",
"type": "module",
"scripts": {
"dev": "vite --port 5175",
"build": "tsc -p . && vite build",
"typecheck": "tsc -p ."
},
"dependencies": {
"@openclapper/design": "workspace:*",
"preact": "^10.27.0"
},
"devDependencies": {
"@preact/preset-vite": "^2.10.0",
"vite": "^7.0.0"
}
}
Binary file added apps/specimen/public/favicon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions apps/specimen/src/cards/brand/brand-clients.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Port of guidelines/brand-clients.card.html. The hex under each environment is the swatch's value, shown as data.
import markProduction from "@openclapper/design/assets/marks/mark-production.svg";
import markStaging from "@openclapper/design/assets/marks/mark-staging.svg";
import markDevelopment from "@openclapper/design/assets/marks/mark-development.svg";
import studioProduction from "@openclapper/design/assets/marks/studio-production.svg";
import studioStaging from "@openclapper/design/assets/marks/studio-staging.svg";
import studioDevelopment from "@openclapper/design/assets/marks/studio-development.svg";
import phoneProduction from "@openclapper/design/assets/marks/phone-production.svg";
import phoneStaging from "@openclapper/design/assets/marks/phone-staging.svg";
import phoneDevelopment from "@openclapper/design/assets/marks/phone-development.svg";
import adminProduction from "@openclapper/design/assets/marks/admin-production.svg";
import adminStaging from "@openclapper/design/assets/marks/admin-staging.svg";
import adminDevelopment from "@openclapper/design/assets/marks/admin-development.svg";
import homeProduction from "@openclapper/design/assets/marks/home-production.svg";
import homeStaging from "@openclapper/design/assets/marks/home-staging.svg";
import homeDevelopment from "@openclapper/design/assets/marks/home-development.svg";

const CLIENTS = ["Mark · org avatar", "Studio", "Phone app", "Admin board", "Home site"];
const ENVS: [name: string, hex: string, srcs: string[]][] = [
["Production", "#1E3FBF", [markProduction, studioProduction, phoneProduction, adminProduction, homeProduction]],
["Staging", "#B8471F", [markStaging, studioStaging, phoneStaging, adminStaging, homeStaging]],
["Development", "#3B7A1A", [markDevelopment, studioDevelopment, phoneDevelopment, adminDevelopment, homeDevelopment]],
];

const c = { display: "grid", gap: 5, justifyItems: "center", font: "12px/1.2 var(--font-mono)", color: "var(--text-secondary)", textAlign: "center" } as const;

export default function Card() {
return (
<div className="one">
<div style={{ display: "grid", gap: 10, alignItems: "center", gridTemplateColumns: "110px repeat(5,1fr)" }}>
<span />
{CLIENTS.map((cl) => <span key={cl} style={c}>{cl}</span>)}
{ENVS.map(([env, hex, srcs]) => [
<span key={env} style={{ ...c, justifyItems: "start", textAlign: "left" }}><b style={{ color: "var(--text)", fontSize: 13 }}>{env}</b>{hex}</span>,
...srcs.map((src, i) => (
<span key={src} style={c}><img style={{ display: "block", borderRadius: "22%" }} src={src} width={72} height={72} alt={`${CLIENTS[i]} ${env.toLowerCase()}`} /></span>
)),
])}
</div>
</div>
);
}
21 changes: 21 additions & 0 deletions apps/specimen/src/cards/brand/brand-mark.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Port of guidelines/brand-mark.card.html.
import mark from "@openclapper/design/assets/marks/mark-production.svg";
import { Pair } from "../../kit";

const word = { font: "700 26px/1 var(--font-sans)", letterSpacing: "-.01em" };
const rd = { display: "block", borderRadius: "22%" };

function Demo() {
return (
<div style={{ display: "grid", gridTemplateColumns: "auto 1fr", gap: 18, alignItems: "center" }}>
<img style={rd} src={mark} width={128} height={128} alt="OpenClapper mark" />
<div style={{ display: "grid", gap: 14 }}>
<span style={{ ...word, display: "flex", alignItems: "center", gap: 10 }}><img style={rd} src={mark} width={36} height={36} alt="" />OpenClapper</span>
<span style={word}>OpenClapper</span>
<span className="oc-meta">Lock-up · wordmark</span>
</div>
</div>
);
}

export default function Card() { return <Pair Demo={Demo} />; }
50 changes: 50 additions & 0 deletions apps/specimen/src/cards/brand/brand-sizes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// Port of guidelines/brand-sizes.card.html.
import studioProduction from "@openclapper/design/assets/marks/studio-production.svg";
import studioProductionSmall from "@openclapper/design/assets/marks/studio-production-small.svg";
import studioStaging from "@openclapper/design/assets/marks/studio-staging.svg";
import studioStagingSmall from "@openclapper/design/assets/marks/studio-staging-small.svg";
import studioDevelopment from "@openclapper/design/assets/marks/studio-development.svg";
import studioDevelopmentSmall from "@openclapper/design/assets/marks/studio-development-small.svg";
import maskProduction from "@openclapper/design/assets/marks/png/studio-production-icon-maskable-512.png";
import maskStaging from "@openclapper/design/assets/marks/png/studio-staging-icon-maskable-512.png";
import maskDevelopment from "@openclapper/design/assets/marks/png/studio-development-icon-maskable-512.png";
import bgProduction from "@openclapper/design/assets/marks/png/phone-production-adaptive-background-432.png";
import fgProduction from "@openclapper/design/assets/marks/png/phone-production-adaptive-foreground-432.png";
import bgStaging from "@openclapper/design/assets/marks/png/phone-staging-adaptive-background-432.png";
import fgStaging from "@openclapper/design/assets/marks/png/phone-staging-adaptive-foreground-432.png";
import bgDevelopment from "@openclapper/design/assets/marks/png/phone-development-adaptive-background-432.png";
import fgDevelopment from "@openclapper/design/assets/marks/png/phone-development-adaptive-foreground-432.png";

interface Env { id: string; full: string; small: string; mask: string; bg: string; fg: string }
const ENVS: Env[] = [
{ id: "production", full: studioProduction, small: studioProductionSmall, mask: maskProduction, bg: bgProduction, fg: fgProduction },
{ id: "staging", full: studioStaging, small: studioStagingSmall, mask: maskStaging, bg: bgStaging, fg: fgStaging },
{ id: "development", full: studioDevelopment, small: studioDevelopmentSmall, mask: maskDevelopment, bg: bgDevelopment, fg: fgDevelopment },
];
/** [label, drawn size, small glyph]: 512 and 192 are drawn at 72 to fit the card; below 48 the small glyph takes over. */
const SIZES: [label: string, px: number, small: boolean][] = [["512", 72, false], ["192", 72, false], ["64", 64, false], ["48", 48, false], ["32", 32, true], ["24", 24, true], ["16", 16, true]];

const c = { display: "grid", gap: 5, justifyItems: "center", font: "10px/1.2 var(--font-mono)", color: "var(--text-secondary)", textAlign: "center" } as const;
const layer = { display: "block", position: "absolute", left: -18, top: -18 } as const;

function Row({ env }: { env: Env }) {
return (
<div style={{ display: "grid", gridTemplateColumns: "repeat(9,auto)", justifyContent: "start", gap: 14, alignItems: "end" }}>
{SIZES.map(([label, px, small]) => (
<span key={label} style={c}><img style={{ display: "block", borderRadius: "22%" }} src={small ? env.small : env.full} width={px} height={px} alt="" />{label}</span>
))}
<span style={c}><img style={{ display: "block", borderRadius: "50%" }} src={env.mask} width={72} height={72} alt="" />maskable</span>
<span style={c}>
<span style={{ position: "relative", display: "block", width: 72, height: 72, borderRadius: "50%", overflow: "hidden" }}>
<img style={layer} src={env.bg} width={108} height={108} alt="" />
<img style={layer} src={env.fg} width={108} height={108} alt="" />
</span>
adaptive
</span>
</div>
);
}

export default function Card() {
return <div className="one" style={{ display: "grid", gap: 16 }}>{ENVS.map((e) => <Row key={e.id} env={e} />)}</div>;
}
21 changes: 21 additions & 0 deletions apps/specimen/src/cards/brand/brand-social.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Port of guidelines/brand-social.card.html.
import social from "@openclapper/design/assets/marks/png/social-preview-1200x630.png";
import favProduction from "@openclapper/design/assets/marks/png/studio-production-favicon-16.png";
import favStaging from "@openclapper/design/assets/marks/png/studio-staging-favicon-16.png";
import favDevelopment from "@openclapper/design/assets/marks/png/studio-development-favicon-16.png";

const tab = { display: "flex", alignItems: "center", gap: 8, height: 28, padding: "0 10px", border: "1px solid var(--border-control)", borderRadius: "6px 6px 0 0", fontSize: 12, width: 170 };

export default function Card() {
return (
<div className="one" style={{ display: "grid", gridTemplateColumns: "1fr auto", gap: 20, alignItems: "start" }}>
<img src={social} style={{ display: "block", width: "100%", height: "auto", border: "1px solid var(--border-divider)" }} alt="Social preview" />
<div style={{ display: "grid", gap: 10 }}>
{[favProduction, favStaging, favDevelopment].map((src) => (
<span key={src} style={tab}><img style={{ display: "block" }} src={src} width={16} height={16} alt="" />OpenClapper Studio</span>
))}
<span className="oc-meta">Browser tabs · 16 px</span>
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions apps/specimen/src/cards/colors/accent-and-selection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Port of guidelines/accent-and-selection.card.html.
import { Pair } from "../../kit";

const TOKENS = ["accent", "accent-hover", "accent-pressed", "accent-tint", "selection", "selection-border", "focus", "marquee"];

function Demo() {
return (
<div className="sw" style={{ gridTemplateColumns: "repeat(4,minmax(0,1fr))" }}>
{TOKENS.map((t) => <div key={t}><i style={{ background: `var(--${t})` }} /><b>{t}</b></div>)}
</div>
);
}

export default function Card() { return <Pair Demo={Demo} />; }
22 changes: 22 additions & 0 deletions apps/specimen/src/cards/colors/boundaries.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Port of guidelines/boundaries.card.html.
import { Pair } from "../../kit";

const EDGES: [label: string, border: string, color: string][] = [
["control", "1px solid var(--border-control)", "var(--text-secondary)"],
["divider", "1px solid var(--border-divider)", "var(--text-secondary)"],
["window", "1px solid var(--border-window)", "var(--text-secondary)"],
["focused", "1px solid var(--border-window-focused)", "var(--text-secondary)"],
["disabled", "1px dashed var(--border-control)", "var(--text-disabled)"],
];

function Demo() {
return (
<div className="r" style={{ margin: 0 }}>
{EDGES.map(([label, border, color]) => (
<span key={label} style={{ display: "grid", placeItems: "center", width: 68, height: 44, border, borderRadius: 4, font: "10px var(--font-mono)", color }}>{label}</span>
))}
</div>
);
}

export default function Card() { return <Pair Demo={Demo} />; }
14 changes: 14 additions & 0 deletions apps/specimen/src/cards/colors/categorical.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Port of guidelines/categorical.card.html.
import { Pair } from "../../kit";

const TOKENS = ["cat-1", "cat-2", "cat-3", "cat-4", "cat-5", "cat-6", "cat-7", "cat-8"];

function Demo() {
return (
<div className="sw" style={{ gridTemplateColumns: "repeat(8,minmax(0,1fr))" }}>
{TOKENS.map((t) => <div key={t}><i style={{ background: `var(--${t})` }} /><b>{t}</b></div>)}
</div>
);
}

export default function Card() { return <Pair Demo={Demo} />; }
70 changes: 70 additions & 0 deletions apps/specimen/src/cards/colors/contrast.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// Port of guidelines/contrast.card.html: every pair's ratio is computed live from the tokens as resolved in each half.
import { useLayoutEffect, useRef, useState } from "preact/hooks";

/** [foreground token, background token, minimum ratio]; 0 = decorative, no minimum. */
const PAIRS: [string, string, number][] = [
["text", "surface", 7], ["text", "surface-sunken", 7], ["text", "surface-bar", 7], ["text", "surface-hover", 7], ["text", "selection", 7], ["text", "field", 7],
["text-on-accent", "accent", 7], ["text-inverse", "surface-inverse", 7],
["accent", "surface", 4.5], ["danger", "surface", 4.5], ["success", "surface", 4.5], ["warning", "surface", 4.5],
["text-secondary", "surface", 4.5], ["text-secondary", "surface-sunken", 4.5], ["text-secondary", "surface-bar", 4.5],
["border-control", "surface", 3], ["border-control", "surface-sunken", 3], ["border-control", "field", 3],
["focus", "surface", 3], ["selection-border", "selection", 3], ["border-window-focused", "bg-desktop", 3],
["border-window", "bg-desktop", 0], ["border-divider", "surface", 0],
["cat-1", "surface", 3], ["cat-2", "surface", 3], ["cat-3", "surface", 3], ["cat-4", "surface", 3], ["cat-5", "surface", 3], ["cat-6", "surface", 3], ["cat-7", "surface", 3], ["cat-8", "surface", 3],
];

/** Resolves a token to sRGB channels by letting the browser compute it inside `el`, so the theme of that subtree applies. */
function rgb(el: HTMLElement, token: string): number[] {
const probe = document.createElement("i");
probe.style.color = `var(--${token})`;
el.appendChild(probe);
const m = (getComputedStyle(probe).color.match(/[\d.]+/g) ?? []).map(Number);
probe.remove();
return m;
}

/** WCAG relative luminance. */
function lum(c: number[]): number {
const [r, g, b] = c.slice(0, 3).map((x) => { x /= 255; return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4); });
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
}

function Table({ theme }: { theme: "light" | "dark" }) {
const ref = useRef<HTMLDivElement>(null);
const [ratios, setRatios] = useState<number[] | null>(null);
useLayoutEffect(() => {
let live = true;
const fill = () => {
const el = ref.current;
if (!el || !live) return;
setRatios(PAIRS.map(([a, b]) => { const x = lum(rgb(el, a)), y = lum(rgb(el, b)); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05); }));
};
fill();
void document.fonts.ready.then(fill);
return () => { live = false; };
}, []);
return (
<div className="half" data-theme={theme} ref={ref}>
<table style={{ fontSize: 10 }}>
<tbody>
<tr><th>foreground / background</th><th>ratio</th><th>min</th><th /></tr>
{ratios && PAIRS.map(([a, b, m], i) => {
const r = ratios[i];
return (
<tr key={`${a}/${b}`}>
<td>{a} / {b}</td><td>{r.toFixed(1)}:1</td>
{m === 0
? <><td>none</td><td style={{ color: "var(--text-secondary)" }}>decorative</td></>
: <><td>{m}</td><td style={{ fontWeight: 700, color: `var(--${r >= m ? "success" : "danger"})` }}>{r >= m ? "pass" : "FAIL"}</td></>}
</tr>
);
})}
</tbody>
</table>
</div>
);
}

export default function Card() {
return <div className="pair"><Table theme="light" /><Table theme="dark" /></div>;
}
18 changes: 18 additions & 0 deletions apps/specimen/src/cards/colors/environments.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// Port of guidelines/environments.card.html. The in-app marker is the source's inline markup (not EnvMarker).
const ENVS: [name: string, token: string][] = [["Production", "env-production"], ["Staging", "env-staging"], ["Development", "env-development"]];

export default function Card() {
return (
<div className="one r" style={{ gap: 20 }}>
{ENVS.map(([name, token]) => (
<span key={token} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<i style={{ width: 48, height: 48, borderRadius: 10, background: `var(--${token})` }} />
<span style={{ display: "grid", gap: 5 }}>
<span style={{ display: "inline-flex", alignItems: "center", height: 20, padding: "0 7px", borderRadius: 3, background: `var(--${token})`, color: "var(--on-env)", font: "700 11px/1 var(--font-sans)" }}>{name}</span>
<span className="oc-num oc-meta">--{token}</span>
</span>
</span>
))}
</div>
);
}
23 changes: 23 additions & 0 deletions apps/specimen/src/cards/colors/over-content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// Port of guidelines/over-content.card.html. The grey gradient stands in for a photo (data, not chrome).
const TOKENS = ["photo-bg", "over-ink", "over-paper", "over-mask", "over-frame", "over-human", "over-rec", "over-pin"];
const tag = { position: "absolute", background: "var(--over-ink)", color: "var(--over-paper)", padding: "3px 4px", borderRadius: 2 } as const;

export default function Card() {
return (
<div className="one" style={{ display: "grid", gridTemplateColumns: "220px 1fr", gap: 16 }}>
<div style={{ position: "relative", height: 120, background: "var(--photo-bg)" }}>
<div style={{ position: "absolute", inset: 0, background: "linear-gradient(160deg,#6a6a6a,#262626 70%)" }} />
<div style={{ position: "absolute", inset: 0, border: "16px solid var(--over-mask)" }} />
<div style={{ position: "absolute", inset: 16, border: "1px solid var(--over-frame)", outline: "1px solid var(--over-frame-outline)" }} />
<span style={{ ...tag, left: 20, bottom: 20, font: "500 10px/1 var(--font-mono)" }}>24 mm</span>
<span style={{ ...tag, right: 20, top: 20, display: "flex", alignItems: "center", gap: 4, font: "700 10px/1 var(--font-sans)" }}>
<i style={{ width: 7, height: 7, borderRadius: 9, background: "var(--over-rec)" }} />REC
</span>
<div style={{ position: "absolute", left: 70, top: 40, width: 60, height: 44, border: "1px dashed var(--over-human)" }} />
</div>
<div className="sw" style={{ gridTemplateColumns: "repeat(4,minmax(0,1fr))" }}>
{TOKENS.map((t) => <div key={t}><i style={{ background: `var(--${t})` }} /><b>{t}</b></div>)}
</div>
</div>
);
}
14 changes: 14 additions & 0 deletions apps/specimen/src/cards/colors/states.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Port of guidelines/states.card.html.
import { Pair } from "../../kit";

const TOKENS = ["success", "success-tint", "warning", "warning-tint", "danger", "danger-tint"];

function Demo() {
return (
<div className="sw" style={{ gridTemplateColumns: "repeat(6,minmax(0,1fr))" }}>
{TOKENS.map((t) => <div key={t}><i style={{ background: `var(--${t})` }} /><b>{t}</b></div>)}
</div>
);
}

export default function Card() { return <Pair Demo={Demo} />; }
Loading
Loading