Skip to content
Open
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
28 changes: 28 additions & 0 deletions .design-sync/NOTES.md
Original file line number Diff line number Diff line change
@@ -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 `<body>`, 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).
16 changes: 16 additions & 0 deletions .design-sync/build-css.mjs
Original file line number Diff line number Diff line change
@@ -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`)

Check failure on line 11 in .design-sync/build-css.mjs

View workflow job for this annotation

GitHub Actions / Lint

'console' is not defined
})
.catch((err) => {
console.error(err)

Check failure on line 14 in .design-sync/build-css.mjs

View workflow job for this annotation

GitHub Actions / Lint

'console' is not defined
process.exit(1)

Check failure on line 15 in .design-sync/build-css.mjs

View workflow job for this annotation

GitHub Actions / Lint

'process' is not defined
})
28 changes: 28 additions & 0 deletions .design-sync/config.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
57 changes: 57 additions & 0 deletions .design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -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 (
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
<CardDescription>Lisa Andersson · Molly</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fredag 7 augusti, 08:00–09:15
</p>
<Badge className="mt-2">Bekräftad</Badge>
</CardContent>
<CardFooter className="gap-2">
<Button size="sm">Se detaljer</Button>
<Button size="sm" variant="outline">Boka om</Button>
</CardFooter>
</Card>
)
}
```
12 changes: 12 additions & 0 deletions .design-sync/previews/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Badge } from "equinet-ui"

export function Variants() {
return (
<div className="flex flex-wrap items-center gap-2 p-4">
<Badge variant="default">Bekräftad</Badge>
<Badge variant="secondary">Väntande</Badge>
<Badge variant="destructive">Avbokad</Badge>
<Badge variant="outline">Genomförd</Badge>
</div>
)
}
35 changes: 35 additions & 0 deletions .design-sync/previews/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { Button } from "equinet-ui"

export function Variants() {
return (
<div className="flex flex-wrap items-center gap-3 p-4">
<Button variant="default">Boka nu</Button>
<Button variant="secondary">Avboka</Button>
<Button variant="outline">Se detaljer</Button>
<Button variant="destructive">Ta bort</Button>
<Button variant="ghost">Avbryt</Button>
<Button variant="link">Läs mer</Button>
</div>
)
}

export function Sizes() {
return (
<div className="flex flex-wrap items-center gap-3 p-4">
<Button size="sm">Liten</Button>
<Button size="default">Standard</Button>
<Button size="lg">Stor</Button>
</div>
)
}

export function Disabled() {
return (
<div className="flex flex-wrap items-center gap-3 p-4">
<Button disabled>Boka nu</Button>
<Button variant="outline" disabled>
Se detaljer
</Button>
</div>
)
}
37 changes: 37 additions & 0 deletions .design-sync/previews/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import {
Card,
CardHeader,
CardTitle,
CardDescription,
CardContent,
CardFooter,
Button,
Badge,
} from "equinet-ui"

export function BookingCard() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
<CardDescription>Lisa Andersson &middot; Molly</CardDescription>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fredag 7 augusti, 08:00&ndash;09:15
</p>
<Badge className="mt-2" variant="default">
Bekräftad
</Badge>
</CardContent>
<CardFooter className="gap-2">
<Button size="sm">Se detaljer</Button>
<Button size="sm" variant="outline">
Boka om
</Button>
</CardFooter>
</Card>
</div>
)
}
19 changes: 19 additions & 0 deletions .design-sync/previews/CardContent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Card, CardHeader, CardTitle, CardContent, Badge } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground">
Fredag 7 augusti, 08:00&ndash;09:15
</p>
<Badge className="mt-2">Bekräftad</Badge>
</CardContent>
</Card>
</div>
)
}
14 changes: 14 additions & 0 deletions .design-sync/previews/CardDescription.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Card, CardHeader, CardTitle, CardDescription } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
<CardDescription>Lisa Andersson &middot; Molly</CardDescription>
</CardHeader>
</Card>
</div>
)
}
19 changes: 19 additions & 0 deletions .design-sync/previews/CardFooter.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Card, CardHeader, CardTitle, CardFooter, Button } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
</CardHeader>
<CardFooter className="gap-2">
<Button size="sm">Se detaljer</Button>
<Button size="sm" variant="outline">
Boka om
</Button>
</CardFooter>
</Card>
</div>
)
}
14 changes: 14 additions & 0 deletions .design-sync/previews/CardHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Card, CardHeader, CardTitle, CardDescription } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
<CardDescription>Lisa Andersson &middot; Molly</CardDescription>
</CardHeader>
</Card>
</div>
)
}
13 changes: 13 additions & 0 deletions .design-sync/previews/CardTitle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Card, CardHeader, CardTitle } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm">
<Card>
<CardHeader>
<CardTitle>Helskoning</CardTitle>
</CardHeader>
</Card>
</div>
)
}
20 changes: 20 additions & 0 deletions .design-sync/previews/Checkbox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Checkbox, Label } from "equinet-ui"

export function States() {
return (
<div className="flex flex-col gap-3 p-4">
<div className="flex items-center gap-2">
<Checkbox id="ds-checkbox-unchecked" />
<Label htmlFor="ds-checkbox-unchecked">Skicka påminnelse</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="ds-checkbox-checked" defaultChecked />
<Label htmlFor="ds-checkbox-checked">Godkänn villkoren</Label>
</div>
<div className="flex items-center gap-2">
<Checkbox id="ds-checkbox-disabled" disabled />
<Label htmlFor="ds-checkbox-disabled">Inaktiverad</Label>
</div>
</div>
)
}
19 changes: 19 additions & 0 deletions .design-sync/previews/Input.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Input, Label } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm space-y-2">
<Label htmlFor="ds-input-email">E-post</Label>
<Input id="ds-input-email" type="email" placeholder="din@email.se" />
</div>
)
}

export function Disabled() {
return (
<div className="p-4 max-w-sm space-y-2">
<Label htmlFor="ds-input-disabled">Telefon</Label>
<Input id="ds-input-disabled" disabled value="070-123 45 67" />
</div>
)
}
10 changes: 10 additions & 0 deletions .design-sync/previews/Label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Label, Input } from "equinet-ui"

export function Default() {
return (
<div className="p-4 max-w-sm space-y-2">
<Label htmlFor="ds-label-name">Hästens namn</Label>
<Input id="ds-label-name" placeholder="Molly" />
</div>
)
}
11 changes: 11 additions & 0 deletions .design-sync/previews/Skeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Skeleton } from "equinet-ui"

export function CardLoading() {
return (
<div className="p-4 max-w-sm space-y-3">
<Skeleton className="h-6 w-2/3" />
<Skeleton className="h-4 w-1/2" />
<Skeleton className="h-24 w-full rounded-lg" />
</div>
)
}
20 changes: 20 additions & 0 deletions .design-sync/previews/Switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Switch, Label } from "equinet-ui"

export function States() {
return (
<div className="flex flex-col gap-3 p-4">
<div className="flex items-center gap-2">
<Switch id="ds-switch-off" />
<Label htmlFor="ds-switch-off">Röstloggning</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="ds-switch-on" defaultChecked />
<Label htmlFor="ds-switch-on">Push-notiser</Label>
</div>
<div className="flex items-center gap-2">
<Switch id="ds-switch-disabled" disabled />
<Label htmlFor="ds-switch-disabled">Inaktiverad</Label>
</div>
</div>
)
}
Loading
Loading