diff --git a/CLAUDE.md b/CLAUDE.md index cb9ba62..f88ee2f 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/Makefile b/Makefile index 72300ee..bfa0410 100644 --- a/Makefile +++ b/Makefile @@ -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) \ @@ -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 diff --git a/apps/specimen/index.html b/apps/specimen/index.html new file mode 100644 index 0000000..32ea305 --- /dev/null +++ b/apps/specimen/index.html @@ -0,0 +1,13 @@ + + + + + +OpenClapper design specimen + + + +
+ + + diff --git a/apps/specimen/package.json b/apps/specimen/package.json new file mode 100644 index 0000000..2e05b38 --- /dev/null +++ b/apps/specimen/package.json @@ -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" + } +} diff --git a/apps/specimen/public/favicon.png b/apps/specimen/public/favicon.png new file mode 100644 index 0000000..2ed4482 Binary files /dev/null and b/apps/specimen/public/favicon.png differ diff --git a/apps/specimen/src/cards/brand/brand-clients.tsx b/apps/specimen/src/cards/brand/brand-clients.tsx new file mode 100644 index 0000000..0b8b8ab --- /dev/null +++ b/apps/specimen/src/cards/brand/brand-clients.tsx @@ -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 ( +
+
+ + {CLIENTS.map((cl) => {cl})} + {ENVS.map(([env, hex, srcs]) => [ + {env}{hex}, + ...srcs.map((src, i) => ( + {`${CLIENTS[i]} + )), + ])} +
+
+ ); +} diff --git a/apps/specimen/src/cards/brand/brand-mark.tsx b/apps/specimen/src/cards/brand/brand-mark.tsx new file mode 100644 index 0000000..e29b818 --- /dev/null +++ b/apps/specimen/src/cards/brand/brand-mark.tsx @@ -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 ( +
+ OpenClapper mark +
+ OpenClapper + OpenClapper + Lock-up · wordmark +
+
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/brand/brand-sizes.tsx b/apps/specimen/src/cards/brand/brand-sizes.tsx new file mode 100644 index 0000000..bde62cf --- /dev/null +++ b/apps/specimen/src/cards/brand/brand-sizes.tsx @@ -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 ( +
+ {SIZES.map(([label, px, small]) => ( + {label} + ))} + maskable + + + + + + adaptive + +
+ ); +} + +export default function Card() { + return
{ENVS.map((e) => )}
; +} diff --git a/apps/specimen/src/cards/brand/brand-social.tsx b/apps/specimen/src/cards/brand/brand-social.tsx new file mode 100644 index 0000000..56d5698 --- /dev/null +++ b/apps/specimen/src/cards/brand/brand-social.tsx @@ -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 ( +
+ Social preview +
+ {[favProduction, favStaging, favDevelopment].map((src) => ( + OpenClapper Studio + ))} + Browser tabs · 16 px +
+
+ ); +} diff --git a/apps/specimen/src/cards/colors/accent-and-selection.tsx b/apps/specimen/src/cards/colors/accent-and-selection.tsx new file mode 100644 index 0000000..421acd8 --- /dev/null +++ b/apps/specimen/src/cards/colors/accent-and-selection.tsx @@ -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 ( +
+ {TOKENS.map((t) =>
{t}
)} +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/colors/boundaries.tsx b/apps/specimen/src/cards/colors/boundaries.tsx new file mode 100644 index 0000000..0b60342 --- /dev/null +++ b/apps/specimen/src/cards/colors/boundaries.tsx @@ -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 ( +
+ {EDGES.map(([label, border, color]) => ( + {label} + ))} +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/colors/categorical.tsx b/apps/specimen/src/cards/colors/categorical.tsx new file mode 100644 index 0000000..b1155b4 --- /dev/null +++ b/apps/specimen/src/cards/colors/categorical.tsx @@ -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 ( +
+ {TOKENS.map((t) =>
{t}
)} +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/colors/contrast.tsx b/apps/specimen/src/cards/colors/contrast.tsx new file mode 100644 index 0000000..1dde87d --- /dev/null +++ b/apps/specimen/src/cards/colors/contrast.tsx @@ -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(null); + const [ratios, setRatios] = useState(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 ( +
+ + + + {ratios && PAIRS.map(([a, b, m], i) => { + const r = ratios[i]; + return ( + + + {m === 0 + ? <> + : <>} + + ); + })} + +
foreground / backgroundratiomin
{a} / {b}{r.toFixed(1)}:1nonedecorative{m}= m ? "success" : "danger"})` }}>{r >= m ? "pass" : "FAIL"}
+
+ ); +} + +export default function Card() { + return
; +} diff --git a/apps/specimen/src/cards/colors/environments.tsx b/apps/specimen/src/cards/colors/environments.tsx new file mode 100644 index 0000000..ef4a36e --- /dev/null +++ b/apps/specimen/src/cards/colors/environments.tsx @@ -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 ( +
+ {ENVS.map(([name, token]) => ( + + + + {name} + --{token} + + + ))} +
+ ); +} diff --git a/apps/specimen/src/cards/colors/over-content.tsx b/apps/specimen/src/cards/colors/over-content.tsx new file mode 100644 index 0000000..79375d2 --- /dev/null +++ b/apps/specimen/src/cards/colors/over-content.tsx @@ -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 ( +
+
+
+
+
+ 24 mm + + REC + +
+
+
+ {TOKENS.map((t) =>
{t}
)} +
+
+ ); +} diff --git a/apps/specimen/src/cards/colors/states.tsx b/apps/specimen/src/cards/colors/states.tsx new file mode 100644 index 0000000..028d979 --- /dev/null +++ b/apps/specimen/src/cards/colors/states.tsx @@ -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 ( +
+ {TOKENS.map((t) =>
{t}
)} +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/colors/surfaces.tsx b/apps/specimen/src/cards/colors/surfaces.tsx new file mode 100644 index 0000000..9a0f51d --- /dev/null +++ b/apps/specimen/src/cards/colors/surfaces.tsx @@ -0,0 +1,14 @@ +// Port of guidelines/surfaces.card.html. +import { Pair } from "../../kit"; + +const TOKENS = ["bg-desktop", "surface", "surface-sunken", "surface-raised", "surface-bar", "surface-hover", "surface-pressed", "field"]; + +function Demo() { + return ( +
+ {TOKENS.map((t) =>
{t}
)} +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/colors/text.tsx b/apps/specimen/src/cards/colors/text.tsx new file mode 100644 index 0000000..85e4e74 --- /dev/null +++ b/apps/specimen/src/cards/colors/text.tsx @@ -0,0 +1,20 @@ +// Port of guidelines/text.card.html. +import { Pair } from "../../kit"; + +const chip = { padding: "2px 6px", borderRadius: 3 }; + +function Demo() { + return ( +
+ text · body and labels, 7:1 + text-secondary · meta, 4.5:1 + text-disabled · with a dashed edge + + text-inverse + text-on-accent + +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/actions.tsx b/apps/specimen/src/cards/components/actions.tsx new file mode 100644 index 0000000..54cec2a --- /dev/null +++ b/apps/specimen/src/cards/components/actions.tsx @@ -0,0 +1,22 @@ +// Port of components/actions/actions.card.html. +import { Button, Link } from "@openclapper/design"; +import { L, Pair } from "../../kit"; + +function Demo() { + return ( +
+ variants +
+ hover · focus · pressed · selected +
+ loading · disabled +
+ icon-only · sizes 24, 28, 36 +
+ link +
LinkHoverExternalDisabled
+
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/charts.tsx b/apps/specimen/src/cards/components/charts.tsx new file mode 100644 index 0000000..1abf64d --- /dev/null +++ b/apps/specimen/src/cards/components/charts.tsx @@ -0,0 +1,22 @@ +// Port of components/charts/charts.card.html. +import { AreaChart, Badge, BarChart, Card as DsCard, DonutChart } from "@openclapper/design"; +import { Pair } from "../../kit"; + +const DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"].map((d, i) => ({ day: d, ext: [42, 57, 38, 64, 71, 22, 15][i], int: [18, 24, 31, 20, 26, 12, 9][i] })); +const MONTHS = ["Jun", "Jul", "Aug", "Sep", "Oct", "Nov"].map((m, i) => ({ m, gb: [4.2, 7.9, 12.4, 18.1, 27.6, 41.0][i], prev: [3.1, 5.0, 8.8, 11.2, 15.9, 22.4][i] })); +const SPLIT = [{ name: "Photos", value: 312 }, { name: "Shots", value: 96 }, { name: "Sketches", value: 48 }, { name: "Timelines", value: 12 }]; + +function Demo() { + return ( +
+ 12 %}> + v + " GB"} series={[{ key: "gb", label: "This project" }, { key: "prev", label: "Last project", color: 2 }]} data={MONTHS} /> +
+ + +
+
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/core.tsx b/apps/specimen/src/cards/components/core.tsx new file mode 100644 index 0000000..a852ae3 --- /dev/null +++ b/apps/specimen/src/cards/components/core.tsx @@ -0,0 +1,19 @@ +// Port of components/core/core.card.html. +import { Avatar, Badge, Divider, Icon, Kbd } from "@openclapper/design"; +import { L, Pair } from "../../kit"; + +function Demo() { + return ( +
+ badge · tone always with an icon +
DraftNewApprovedUnreviewedFailed12
+ keyboard hint · avatar +
+ icon 12, 16, 20 · divider +
+ +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/data.tsx b/apps/specimen/src/cards/components/data.tsx new file mode 100644 index 0000000..84155d2 --- /dev/null +++ b/apps/specimen/src/cards/components/data.tsx @@ -0,0 +1,37 @@ +// Port of components/data/data.card.html. +import { Button, EmptyState, List, ListRow, MapControl, MapPin, Table, ThumbnailTile } from "@openclapper/design"; +import { useState } from "preact/hooks"; +import { L, Pair } from "../../kit"; + +const Ph = () => ; +const ROWS = [{ id: "1", name: "IMG_0142.jpg", lens: "24", size: "4.2" }, { id: "2", name: "IMG_0143.jpg", lens: "35", size: "3.9" }, { id: "3", name: "IMG_0147.jpg", lens: "85", size: "5.1" }]; +type Row = (typeof ROWS)[number]; + +function Demo() { + const [sel, setSel] = useState(["2"]); + const [sort, setSort] = useState<{ key: string; dir: "asc" | "desc" }>({ key: "name", dir: "asc" }); + const rows = [...ROWS].sort((a, b) => (a[sort.key as keyof Row] > b[sort.key as keyof Row] ? 1 : -1) * (sort.dir === "asc" ? 1 : -1)); + return ( +
+ list · rest, selected, cursor, chevron + ScoutingDay 2Ref_04.jpgCrew + table · sort, select, mono numbers +
+ thumbnail tiles · map +
+ + + +
+
+ 12 + +
+
+ empty state + Import} style={{ padding: "8px 0" }}>Photos you import appear here. +
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/feedback.tsx b/apps/specimen/src/cards/components/feedback.tsx new file mode 100644 index 0000000..58e92a3 --- /dev/null +++ b/apps/specimen/src/cards/components/feedback.tsx @@ -0,0 +1,18 @@ +// Port of components/feedback/feedback.card.html. +import { Button, ProgressBar, Skeleton, Spinner, Tooltip } from "@openclapper/design"; +import { L, Pair } from "../../kit"; + +function Demo() { + return ( +
+ tooltip with shortcut · spinner +
+ progress · determinate, indeterminate +
+ skeleton +
+
+ ); +} + +export default function Card() { return ; } diff --git a/apps/specimen/src/cards/components/forms.tsx b/apps/specimen/src/cards/components/forms.tsx new file mode 100644 index 0000000..9d3150a --- /dev/null +++ b/apps/specimen/src/cards/components/forms.tsx @@ -0,0 +1,37 @@ +// Port of components/forms/forms.card.html. +import { Checkbox, Chip, Combobox, NumberInput, PasswordInput, Radio, SearchInput, Segmented, Select, Slider, Switch, TextArea, TextInput } from "@openclapper/design"; +import { useState } from "preact/hooks"; +import { L, Pair } from "../../kit"; + +const OPTS = [{ value: "ff", label: "Full frame" }, { value: "s35", label: "Super 35" }, { value: "mft", label: "Four Thirds" }]; + +function Demo() { + const [n, setN] = useState(24); + const [q, setQ] = useState(""); + const [v, setV] = useState("grid"); + const [c, setC] = useState(true); + const [s, setS] = useState(40); + const [o, setO] = useState("s35"); + const w = { width: 132 }; + return ( +
+ text · hover · focus · invalid · disabled +
+ number with unit · password · search +
+ select · combobox (cursor outline, chosen check) +