From bbd548dd9b9d3c3710e29e60b8bfff328f62dff8 Mon Sep 17 00:00:00 2001 From: Johan Lindengard Date: Fri, 7 Aug 2026 13:38:22 +0200 Subject: [PATCH 1/2] feat(design-sync): sync 9 ui primitives to claude.ai/design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Första design-sync-körningen: Button, Badge, Card (+ delar), Input, Label, Textarea, Checkbox, Switch, Skeleton synkade till Equinet- projektet på claude.ai/design med riktiga previews och Tailwind-CSS. Equinet saknar ett fristående komponentbibliotek-bygge, så synken använder en handskriven barrel-entry (synth-entry.ts) + componentSrcMap istället för .d.ts-baserad discovery, och en egen PostCSS-build (build-css.mjs) för att generera CSS från globals.css. Kända gap (saknat Inter-typsnitt, dolda Radix-tokens) dokumenterade i NOTES.md. Co-Authored-By: Claude Sonnet 5 --- .design-sync/NOTES.md | 28 +++++++++++ .design-sync/build-css.mjs | 16 +++++++ .design-sync/config.json | 28 +++++++++++ .design-sync/conventions.md | 57 +++++++++++++++++++++++ .design-sync/previews/Badge.tsx | 12 +++++ .design-sync/previews/Button.tsx | 35 ++++++++++++++ .design-sync/previews/Card.tsx | 37 +++++++++++++++ .design-sync/previews/CardContent.tsx | 19 ++++++++ .design-sync/previews/CardDescription.tsx | 14 ++++++ .design-sync/previews/CardFooter.tsx | 19 ++++++++ .design-sync/previews/CardHeader.tsx | 14 ++++++ .design-sync/previews/CardTitle.tsx | 13 ++++++ .design-sync/previews/Checkbox.tsx | 20 ++++++++ .design-sync/previews/Input.tsx | 19 ++++++++ .design-sync/previews/Label.tsx | 10 ++++ .design-sync/previews/Skeleton.tsx | 11 +++++ .design-sync/previews/Switch.tsx | 20 ++++++++ .design-sync/previews/Textarea.tsx | 25 ++++++++++ .design-sync/synth-entry.ts | 20 ++++++++ .gitignore | 7 +++ 20 files changed, 424 insertions(+) create mode 100644 .design-sync/NOTES.md create mode 100644 .design-sync/build-css.mjs create mode 100644 .design-sync/config.json create mode 100644 .design-sync/conventions.md create mode 100644 .design-sync/previews/Badge.tsx create mode 100644 .design-sync/previews/Button.tsx create mode 100644 .design-sync/previews/Card.tsx create mode 100644 .design-sync/previews/CardContent.tsx create mode 100644 .design-sync/previews/CardDescription.tsx create mode 100644 .design-sync/previews/CardFooter.tsx create mode 100644 .design-sync/previews/CardHeader.tsx create mode 100644 .design-sync/previews/CardTitle.tsx create mode 100644 .design-sync/previews/Checkbox.tsx create mode 100644 .design-sync/previews/Input.tsx create mode 100644 .design-sync/previews/Label.tsx create mode 100644 .design-sync/previews/Skeleton.tsx create mode 100644 .design-sync/previews/Switch.tsx create mode 100644 .design-sync/previews/Textarea.tsx create mode 100644 .design-sync/synth-entry.ts diff --git a/.design-sync/NOTES.md b/.design-sync/NOTES.md new file mode 100644 index 00000000..a80e57cf --- /dev/null +++ b/.design-sync/NOTES.md @@ -0,0 +1,28 @@ +# design-sync notes — Equinet + +## Repo shape + +Equinet is a Next.js app, not a standalone component-library package — no `dist/`, no Storybook, no `main`/`module`/`exports` in `package.json`. This sync uses a hand-authored barrel entry (`.design-sync/synth-entry.ts`) + `cfg.componentSrcMap` to declare exactly which components are in scope, instead of relying on `.d.ts`-driven discovery. First sync scoped to 9 pure `src/components/ui` primitives (14 exports counting `Card`'s sub-parts): Button, Badge, Card/CardHeader/CardTitle/CardDescription/CardContent/CardFooter, Input, Label, Textarea, Checkbox, Switch, Skeleton. + +To add a component to the sync: add an export line to `.design-sync/synth-entry.ts` and an entry to `cfg.componentSrcMap`, then rebuild. + +## CSS + +No compiled Tailwind stylesheet exists anywhere in the repo (Tailwind v4, CSS-first config via `@tailwindcss/postcss`). `.design-sync/build-css.mjs` runs PostCSS + `@tailwindcss/postcss` directly against `src/app/globals.css` to produce `.design-sync/.cache/compiled.css`, which `cfg.cssEntry` points at. Tailwind v4's automatic content detection scans the whole project from `globals.css`'s location, so the compiled CSS includes utility classes used anywhere in the app — safe (superset), but means the file is larger (~146KB) than strictly needed for the 9 synced components. Re-run `node .design-sync/build-css.mjs` before every rebuild if any Tailwind class usage changed (wired as `cfg.buildCmd` for documentation — not auto-executed by resync.mjs). + +## Known, accepted gaps (non-blocking) + +- **`[TOKENS_MISSING]`: `--font-geist-sans` / `--font-geist-mono`** — dead/vestigial CSS variable references left in `globals.css`'s `@theme inline` block from the original `create-next-app` scaffold. The app actually uses `Inter` (body) and `DM_Serif_Display` (headings) via `next/font/google`, applied as a generated `className` directly on ``, not through these vars. Real consequence: text in the Claude Design previews renders in the browser's fallback sans-serif, not Inter — `next/font` self-hosted fonts aren't portable to a standalone bundle (build-hashed filenames, no stable static path). Accepted as-is; not chased further for this sync. If this needs fixing later: either clean up the dead `--font-geist-*` refs in `globals.css`, or self-host an Inter woff2 in the repo and wire it via `cfg.extraFonts`. +- **`[TOKENS_MISSING]`: `--radix-select-*` / `--radix-dropdown-menu-*`** — set at runtime by Radix's Select/DropdownMenu primitives via inline styles. Not used by any of the 9 synced components (no Select/DropdownMenu in scope yet) — irrelevant until those are added to a future sync. +- **Badge `destructive` variant contrast** — `text-destructive-foreground` on `bg-destructive` looked low-contrast in the review screenshot. `--destructive-foreground` is not defined in `globals.css` (only `--destructive` is) — this reflects the real app's token set, not a sync artifact. Worth flagging to the app's design owner separately; out of scope to fix here. + +## Scope leak caught before upload + +First `resync.mjs` run swept **the entire `docs/` tree** (architecture reviews, iOS reviews, payment-domain notes, roadmap, decision log — 41 files) into `guidelines/` via the default `guidelinesGlob` (`docs/*.md`, `docs/guides/**/*.md`), because the hand-authored `--entry` override resolves `PKG_DIR` to the repo root. **Fixed** by setting `cfg.guidelinesGlob: []` — this repo has no actual design-guideline docs to sync yet. If real design guidelines get written later (e.g. `docs/design/*.md`), point `guidelinesGlob` at that specific path — never leave it at the default in a repo where `PKG_DIR` is the whole app. + +## Re-sync risks + +- `cfg.componentSrcMap` is the only source of truth for which components are in scope — no `.d.ts` discovery is happening (confirmed empty `exported PascalCase symbols: 0` in every build log). Adding a new `src/components/ui/*.tsx` file does **not** auto-appear in the sync; it must be added to both `synth-entry.ts` and `componentSrcMap` by hand. +- `.design-sync/.cache/compiled.css` is gitignored and regenerated by `build-css.mjs` — if it's stale (Tailwind classes changed in the app but this wasn't re-run), the next build will ship outdated styling silently. Re-run it as part of every re-sync. +- The 9 scoped components have zero React-context dependencies today. The moment a context-dependent component (Dialog, Select, DropdownMenu, ResponsiveDialog's `isMobile` context, etc.) is added to the scope, `cfg.provider` will very likely need to be set — none of that has been figured out yet. +- Toolchain versions used for this sync: Node (system default, no `.nvmrc` pin followed), `@playwright/test` 1.56.1 pinned in repo (chromium build v1200 installed to match), esbuild + ts-morph installed fresh into `.ds-sync/` (not version-pinned beyond `npm i`'s resolution at sync time). diff --git a/.design-sync/build-css.mjs b/.design-sync/build-css.mjs new file mode 100644 index 00000000..2c961fa0 --- /dev/null +++ b/.design-sync/build-css.mjs @@ -0,0 +1,16 @@ +import postcss from "postcss" +import tailwindcss from "@tailwindcss/postcss" +import { readFileSync, writeFileSync } from "node:fs" + +const input = readFileSync("src/app/globals.css", "utf8") + +postcss([tailwindcss()]) + .process(input, { from: "src/app/globals.css", to: ".design-sync/.cache/compiled.css" }) + .then((result) => { + writeFileSync(".design-sync/.cache/compiled.css", result.css) + console.log(`Wrote ${result.css.length} bytes`) + }) + .catch((err) => { + console.error(err) + process.exit(1) + }) diff --git a/.design-sync/config.json b/.design-sync/config.json new file mode 100644 index 00000000..56680588 --- /dev/null +++ b/.design-sync/config.json @@ -0,0 +1,28 @@ +{ + "projectId": "e77a0d60-26e5-4506-b7ed-b21f48a54424", + "pkg": "equinet-ui", + "globalName": "Equinet", + "shape": "package", + "entry": ".design-sync/synth-entry.ts", + "tsconfig": "tsconfig.json", + "buildCmd": "node .design-sync/build-css.mjs", + "cssEntry": ".design-sync/.cache/compiled.css", + "readmeHeader": ".design-sync/conventions.md", + "guidelinesGlob": [], + "componentSrcMap": { + "Button": "src/components/ui/button.tsx", + "Badge": "src/components/ui/badge.tsx", + "Card": "src/components/ui/card.tsx", + "CardHeader": "src/components/ui/card.tsx", + "CardTitle": "src/components/ui/card.tsx", + "CardDescription": "src/components/ui/card.tsx", + "CardContent": "src/components/ui/card.tsx", + "CardFooter": "src/components/ui/card.tsx", + "Input": "src/components/ui/input.tsx", + "Label": "src/components/ui/label.tsx", + "Textarea": "src/components/ui/textarea.tsx", + "Checkbox": "src/components/ui/checkbox.tsx", + "Switch": "src/components/ui/switch.tsx", + "Skeleton": "src/components/ui/skeleton.tsx" + } +} diff --git a/.design-sync/conventions.md b/.design-sync/conventions.md new file mode 100644 index 00000000..dc9c6dbd --- /dev/null +++ b/.design-sync/conventions.md @@ -0,0 +1,57 @@ +## Wrapping and setup + +No provider or root wrapper is required — none of the synced components read from React context. Design tokens are plain CSS custom properties on `:root` (and `.dark` for dark mode), already included in `styles.css`. To preview dark mode, add the `dark` class to an ancestor element; no extra setup needed. + +## Styling idiom + +This is a Tailwind CSS v4 + shadcn/ui system, styled entirely through utility classes — never inline styles or CSS-in-JS. Variant-bearing components (`Button`, `Badge`) use `class-variance-authority`: pass `variant`/`size` props, never hand-compose utility classes for variant state. Every component accepts a `className` prop that merges on top of its defaults (via `cn()` / `tailwind-merge`) — extend spacing/layout this way, don't override variant colors by className. + +Real token vocabulary (all defined in `styles.css`, both light and dark): + +| Token | Utility class examples | +|---|---| +| `--primary` / `--primary-foreground` | `bg-primary`, `text-primary-foreground` | +| `--secondary` / `--secondary-foreground` | `bg-secondary`, `text-secondary-foreground` | +| `--destructive` | `bg-destructive`, `text-destructive` | +| `--muted` / `--muted-foreground` | `bg-muted`, `text-muted-foreground` | +| `--card` / `--card-foreground` | `bg-card`, `text-card-foreground` | +| `--border` / `--input` / `--ring` | `border`, `border-input`, `ring-ring` | +| `--radius` (+ `-sm`/`-md`/`-lg`/`-xl` scale) | `rounded-md`, `rounded-lg`, `rounded-xl` | + +Brand primary is a dark green (`oklch(0.42 0.09 160)`), not the shadcn default — always use `bg-primary`/`variant="default"` rather than a hardcoded green. + +## Where the truth lives + +- `styles.css` — root token definitions (`:root`, `.dark`) plus the `@import` of `_ds_bundle.css`, which carries the compiled component CSS. +- Each component's `.prompt.md` — usage reference for that component specifically. +- `_ds_bundle.js` — the real compiled components (`Button`, `Badge`, `Card`/`CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`, `Input`, `Label`, `Textarea`, `Checkbox`, `Switch`, `Skeleton`). + +## Example composition + +```tsx +import { + Card, CardHeader, CardTitle, CardDescription, + CardContent, CardFooter, Button, Badge, +} from "equinet-ui" + +function BookingCard() { + return ( + + + Helskoning + Lisa Andersson · Molly + + +

+ Fredag 7 augusti, 08:00–09:15 +

+ Bekräftad +
+ + + + +
+ ) +} +``` diff --git a/.design-sync/previews/Badge.tsx b/.design-sync/previews/Badge.tsx new file mode 100644 index 00000000..37567b58 --- /dev/null +++ b/.design-sync/previews/Badge.tsx @@ -0,0 +1,12 @@ +import { Badge } from "equinet-ui" + +export function Variants() { + return ( +
+ Bekräftad + Väntande + Avbokad + Genomförd +
+ ) +} diff --git a/.design-sync/previews/Button.tsx b/.design-sync/previews/Button.tsx new file mode 100644 index 00000000..3f3894e1 --- /dev/null +++ b/.design-sync/previews/Button.tsx @@ -0,0 +1,35 @@ +import { Button } from "equinet-ui" + +export function Variants() { + return ( +
+ + + + + + +
+ ) +} + +export function Sizes() { + return ( +
+ + + +
+ ) +} + +export function Disabled() { + return ( +
+ + +
+ ) +} diff --git a/.design-sync/previews/Card.tsx b/.design-sync/previews/Card.tsx new file mode 100644 index 00000000..22d8b3d9 --- /dev/null +++ b/.design-sync/previews/Card.tsx @@ -0,0 +1,37 @@ +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + CardFooter, + Button, + Badge, +} from "equinet-ui" + +export function BookingCard() { + return ( +
+ + + Helskoning + Lisa Andersson · Molly + + +

+ Fredag 7 augusti, 08:00–09:15 +

+ + Bekräftad + +
+ + + + +
+
+ ) +} diff --git a/.design-sync/previews/CardContent.tsx b/.design-sync/previews/CardContent.tsx new file mode 100644 index 00000000..62bb43be --- /dev/null +++ b/.design-sync/previews/CardContent.tsx @@ -0,0 +1,19 @@ +import { Card, CardHeader, CardTitle, CardContent, Badge } from "equinet-ui" + +export function Default() { + return ( +
+ + + Helskoning + + +

+ Fredag 7 augusti, 08:00–09:15 +

+ Bekräftad +
+
+
+ ) +} diff --git a/.design-sync/previews/CardDescription.tsx b/.design-sync/previews/CardDescription.tsx new file mode 100644 index 00000000..7fc1b7ee --- /dev/null +++ b/.design-sync/previews/CardDescription.tsx @@ -0,0 +1,14 @@ +import { Card, CardHeader, CardTitle, CardDescription } from "equinet-ui" + +export function Default() { + return ( +
+ + + Helskoning + Lisa Andersson · Molly + + +
+ ) +} diff --git a/.design-sync/previews/CardFooter.tsx b/.design-sync/previews/CardFooter.tsx new file mode 100644 index 00000000..c364503d --- /dev/null +++ b/.design-sync/previews/CardFooter.tsx @@ -0,0 +1,19 @@ +import { Card, CardHeader, CardTitle, CardFooter, Button } from "equinet-ui" + +export function Default() { + return ( +
+ + + Helskoning + + + + + + +
+ ) +} diff --git a/.design-sync/previews/CardHeader.tsx b/.design-sync/previews/CardHeader.tsx new file mode 100644 index 00000000..7fc1b7ee --- /dev/null +++ b/.design-sync/previews/CardHeader.tsx @@ -0,0 +1,14 @@ +import { Card, CardHeader, CardTitle, CardDescription } from "equinet-ui" + +export function Default() { + return ( +
+ + + Helskoning + Lisa Andersson · Molly + + +
+ ) +} diff --git a/.design-sync/previews/CardTitle.tsx b/.design-sync/previews/CardTitle.tsx new file mode 100644 index 00000000..af8a66bd --- /dev/null +++ b/.design-sync/previews/CardTitle.tsx @@ -0,0 +1,13 @@ +import { Card, CardHeader, CardTitle } from "equinet-ui" + +export function Default() { + return ( +
+ + + Helskoning + + +
+ ) +} diff --git a/.design-sync/previews/Checkbox.tsx b/.design-sync/previews/Checkbox.tsx new file mode 100644 index 00000000..3f0eae24 --- /dev/null +++ b/.design-sync/previews/Checkbox.tsx @@ -0,0 +1,20 @@ +import { Checkbox, Label } from "equinet-ui" + +export function States() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} diff --git a/.design-sync/previews/Input.tsx b/.design-sync/previews/Input.tsx new file mode 100644 index 00000000..33381e9d --- /dev/null +++ b/.design-sync/previews/Input.tsx @@ -0,0 +1,19 @@ +import { Input, Label } from "equinet-ui" + +export function Default() { + return ( +
+ + +
+ ) +} + +export function Disabled() { + return ( +
+ + +
+ ) +} diff --git a/.design-sync/previews/Label.tsx b/.design-sync/previews/Label.tsx new file mode 100644 index 00000000..9c42fea0 --- /dev/null +++ b/.design-sync/previews/Label.tsx @@ -0,0 +1,10 @@ +import { Label, Input } from "equinet-ui" + +export function Default() { + return ( +
+ + +
+ ) +} diff --git a/.design-sync/previews/Skeleton.tsx b/.design-sync/previews/Skeleton.tsx new file mode 100644 index 00000000..b4baf473 --- /dev/null +++ b/.design-sync/previews/Skeleton.tsx @@ -0,0 +1,11 @@ +import { Skeleton } from "equinet-ui" + +export function CardLoading() { + return ( +
+ + + +
+ ) +} diff --git a/.design-sync/previews/Switch.tsx b/.design-sync/previews/Switch.tsx new file mode 100644 index 00000000..64305f12 --- /dev/null +++ b/.design-sync/previews/Switch.tsx @@ -0,0 +1,20 @@ +import { Switch, Label } from "equinet-ui" + +export function States() { + return ( +
+
+ + +
+
+ + +
+
+ + +
+
+ ) +} diff --git a/.design-sync/previews/Textarea.tsx b/.design-sync/previews/Textarea.tsx new file mode 100644 index 00000000..2fd1a754 --- /dev/null +++ b/.design-sync/previews/Textarea.tsx @@ -0,0 +1,25 @@ +import { Textarea, Label } from "equinet-ui" + +export function Default() { + return ( +
+ +