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
37 changes: 37 additions & 0 deletions .design-sync/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# design-sync notes — coven-cave

- **App repo, not a library**: no dist/, no Storybook. Synth-entry mode with `entry: "./dist/index.js"` deliberately pointing at a nonexistent path — that anchors PKG_DIR at the repo root (the entry-override walk-up) while still triggering synth-entry. Do not "fix" the path.
- **Component discovery is fully enumerated** in `componentSrcMap` (41 components): with no `.d.ts` exports, any single pin suppresses `deriveComponentsFromSrc`, so the map must name every component. New ui/ primitives must be added there to sync.
- **Icon ships via `extraEntries`** (`./src/lib/icon.tsx`) — it lives outside `srcDir` so the synth entry doesn't include it. The `[EXPORT_COLLISION] Icon` warn at build is a false positive (the "main package" side is the component-list entry, not a real binding); Icon's only real binding comes from extraEntries.
- **CSS must be pre-compiled through Tailwind v4**: `src/app/globals.css` is a CSS-first Tailwind entry (`@import "tailwindcss"`) and ui/ primitives use utility classes. `buildCmd` = `node .design-sync/build-css.mjs` → `.design-sync/.cache/globals.compiled.css` (cssEntry). The script resolves `postcss` through `@tailwindcss/postcss`'s dep tree (pnpm exposes only direct deps).
- **Fonts**: the app injects `--font-*` via next/font at runtime. `.design-sync/fonts.css` (authored, prepended by build-css.mjs) defines all 25 `--font-*` vars + Google Fonts remote @import for the canonical trio (EB Garamond / Inter / JetBrains Mono) + Plus Jakarta Sans + IBM Plex Mono. Validate reports `[FONT_REMOTE]` — expected, families load at runtime.
- **Playwright**: repo pins playwright@1.61.1 (chromium-1228, matches local `~/Library/Caches/ms-playwright`). pnpm doesn't hoist it to root `node_modules`, so it's installed into `.ds-sync/` (`npm i playwright@1.61.1` there). Re-do on fresh clone.
- **Icon bare-name fix**: the app renders `<Icon name="cat">` with BARE names but the iconify subset registers under prefix `ph` — in the standalone bundle every bare lookup misses (empty `<span>`; the live app has some runtime affordance that makes it work). Fix: `.design-sync/icon-bootstrap.tsx` in `extraEntries` re-registers the subset with `prefix: ""` so simple-name storage/lookup are symmetric. Without it, every icon in every preview is invisible.
- **Preview authoring recipe**: wrap each cell in a `Surface` div with `background: var(--background)`, padding, `borderRadius: var(--radius-card)` — default palette is coven DARK and card bodies are white, so unwrapped components float on white. Import from `"coven-cave"`. Use domain copy (familiar/summon/grimoire) and real icon names from `src/lib/icon.tsx` ICON_NAMES.
- Themes are attribute-driven: `data-theme` (21 palettes) × `data-mode` (dark|light) on `<html>`; tokens all defined in globals.css `:root` + theme blocks. Default with no attributes = coven dark.

## Component preview quirks (folded from wave 1 learnings)

- **Overlay open-state recipes** (all render statically): components owning their open state (OverflowMenu, AvatarLightbox) → host div ref + `useEffect(() => ref.current?.querySelector("button")?.click(), [])`; ConfirmProvider → child calls `useConfirm()` on mount, never resolves; Popover → `useState(true)` + Button (forwardRef) as anchor; ContextMenu → static `{x,y}`; UndoToast → `durationMs={60000}` so the countdown bar is still full; SeparatorHandle dragging → pass `className="ui-sep-handle--dragging"`.
- **Icon names**: both bare ("trash") and `ph:`-prefixed forms resolve at runtime (bootstrap registers both); the app's TS types want `ph:`-prefixed for some props (SearchInput default is `"ph:magnifying-glass"`) — esbuild preview compile doesn't typecheck, so bare names pass.
- **TextInput/TextArea have no standalone chrome** — all label/error styling comes from Field context; preview them inside Field only. Field's `error` prop cascades invalid styling automatically.
- **IconButton `danger` is hover-only** (no at-rest tint) — static captures show it identical to plain; real behavior.
- **RelativeTime** flips to absolute dates past ~a week — keep preview timestamps within days of now.
- **TrendChart y-domain is 0-anchored** — high-baseline metrics (88–99%) pin to the top; use counts/queue-depth data.
- **PulseBars collapses to 0 width in a bare flex row** (grid-auto-columns:1fr) — needs fixed-width wrappers.
- **Sparkline's `.spark`/`.spark-tip` CSS lives in `src/styles/dashboard.css`**, not imported by the component → absent from the bundle. Static render fine (SVG attrs); hover tooltip would be unpositioned.
- **SettingControlRow** is a bundle export (same file as Segmented) but not carded — covered via the SettingsGroup preview.
- Charts wrap themselves in ParentSize — a width-constrained parent suffices.
- Skipped-by-design states: hover reveals, focus rings, shimmer/pulse animations, ColorPicker drag, Popover keyboard-clamp placements.

## Known render warns

- `[TOKENS_MISSING]` (~14 vars): `--toast-accent` (set at runtime via inline style by inbox-toast), `--x` (JS-set), `--accent-rose`, `--bg-sunken`, `--danger-bg/-border/-text`, `--accent-contrast` etc. — referenced by app surfaces (inbox-toast, craft-dossier, projects-view) but never defined in the repo either; pre-existing app quirk, not a bundle defect.
- `[EXPORT_COLLISION] Icon` — false positive, see above.

## Re-sync risks

- `.design-sync/fonts.css` enumerates the next/font variables by hand — if `src/app/fonts.ts` adds/renames families, the map goes stale ([TOKENS_MISSING] on new `--font-*` names is the signal).
- `componentSrcMap` is a hand enumeration — new/renamed ui/ components silently don't sync until added.
- Google Fonts remote @import is a network dependency at render time.
- Tailwind compile scans the whole repo for utility usage (base = repo root); output changes when any app source adds new utility classes — harmless but expect `_ds_bundle.css` churn.
28 changes: 28 additions & 0 deletions .design-sync/build-css.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// design-sync CSS pre-build: compile src/app/globals.css (a Tailwind v4
// CSS-first entry — `@import "tailwindcss"` + utility classes in ui/ sources)
// into a static stylesheet the converter can ship. Run from the repo root
// before .ds-sync/package-build.mjs; cfg.cssEntry points at the output.
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { createRequire } from "node:module";

const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
// pnpm exposes only direct deps at the root; postcss is transitive — resolve
// it through @tailwindcss/postcss's own dependency tree.
const rootRequire = createRequire(resolve(root, "package.json"));
const twPath = rootRequire.resolve("@tailwindcss/postcss");
const tailwindcss = (await import(twPath)).default;
const postcss = (await import(createRequire(twPath).resolve("postcss"))).default;
const from = resolve(root, "src/app/globals.css");
const to = resolve(root, ".design-sync/.cache/globals.compiled.css");

const css = readFileSync(from, "utf8");
const result = await postcss([tailwindcss({ base: root })]).process(css, { from, to });
// Prepend the authored font layer (.design-sync/fonts.css): the app gets its
// --font-* variables from next/font at runtime, which previews/designs don't
// have. Its @import must precede all other rules.
const fonts = readFileSync(resolve(root, ".design-sync/fonts.css"), "utf8");
mkdirSync(dirname(to), { recursive: true });
writeFileSync(to, fonts + "\n" + result.css);
console.error(`✓ compiled ${from} → ${to} (${Math.round(result.css.length / 1024)} KB)`);
69 changes: 69 additions & 0 deletions .design-sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"projectId": "530c9c44-8b06-4dfc-9215-ed70af0a743a",
"shape": "package",
"pkg": "coven-cave",
"globalName": "CovenCave",
"entry": "./dist/index.js",
"srcDir": "src/components/ui",
"tsconfig": "tsconfig.json",
"buildCmd": "node .design-sync/build-css.mjs",
"cssEntry": ".design-sync/.cache/globals.compiled.css",
"guidelinesGlob": "docs/coven-design-language.md",
"readmeHeader": ".design-sync/conventions.md",
"extraEntries": ["./src/lib/icon.tsx", "./.design-sync/icon-bootstrap.tsx"],
"overrides": {
"Modal": { "cardMode": "single", "viewport": "860x620" },
"ConfirmProvider": { "cardMode": "single", "viewport": "860x620" },
"AvatarLightbox": { "cardMode": "single", "viewport": "860x620" },
"Popover": { "cardMode": "single", "viewport": "760x440" },
"ContextMenu": { "cardMode": "single", "viewport": "760x440" },
"OverflowMenu": { "cardMode": "single", "viewport": "760x440" },
"UndoToast": { "cardMode": "single", "viewport": "760x360" },
"ViewHeader": { "cardMode": "column" },
"SelectionToolbar": { "cardMode": "column" },
"Tabs": { "cardMode": "column" }
},
"componentSrcMap": {
"Icon": "src/lib/icon.tsx",
"AiToggle": "src/components/ui/ai-toggle.tsx",
"AuthedImage": "src/components/ui/authed-image.tsx",
"AvatarLightbox": "src/components/ui/avatar-lightbox.tsx",
"Button": "src/components/ui/button.tsx",
"BarChart": "src/components/ui/charts/bar-chart.tsx",
"DonutChart": "src/components/ui/charts/donut-chart.tsx",
"Heatmap": "src/components/ui/charts/heatmap.tsx",
"TrendChart": "src/components/ui/charts/trend-chart.tsx",
"ColorPicker": "src/components/ui/color-picker.tsx",
"ConfirmProvider": "src/components/ui/confirm-dialog.tsx",
"ContextMenu": "src/components/ui/context-menu.tsx",
"EmptyState": "src/components/ui/empty-state.tsx",
"ErrorState": "src/components/ui/error-state.tsx",
"Field": "src/components/ui/field.tsx",
"IconButton": "src/components/ui/icon-button.tsx",
"LifecycleBadge": "src/components/ui/lifecycle-badge.tsx",
"ListRowCard": "src/components/ui/list-row-card.tsx",
"LiveRegionProvider": "src/components/ui/live-region.tsx",
"Modal": "src/components/ui/modal.tsx",
"OriginChip": "src/components/ui/origin-chip.tsx",
"OverflowMenu": "src/components/ui/overflow-menu.tsx",
"Popover": "src/components/ui/popover.tsx",
"PropertyPill": "src/components/ui/property-pill.tsx",
"PulseBars": "src/components/ui/pulse-bars.tsx",
"RelativeTime": "src/components/ui/relative-time.tsx",
"SearchInput": "src/components/ui/search-input.tsx",
"StandardSelect": "src/components/ui/select.tsx",
"SelectionToolbar": "src/components/ui/selection-toolbar.tsx",
"SeparatorHandle": "src/components/ui/separator-handle.tsx",
"SessionInitiatorChip": "src/components/ui/session-initiator-chip.tsx",
"Segmented": "src/components/ui/settings-controls.tsx",
"SettingsGroup": "src/components/ui/settings-group.tsx",
"Skeleton": "src/components/ui/skeleton.tsx",
"Sparkline": "src/components/ui/sparkline.tsx",
"Tabs": "src/components/ui/tabs.tsx",
"TextArea": "src/components/ui/text-area.tsx",
"TextInput": "src/components/ui/text-input.tsx",
"ThinkingIndicator": "src/components/ui/thinking-indicator.tsx",
"UndoToast": "src/components/ui/undo-toast.tsx",
"ViewHeader": "src/components/ui/view-header.tsx"
}
}
53 changes: 53 additions & 0 deletions .design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Building with Coven Cave (OpenCoven)

Coven Cave is a dark, dense, lavender-inked desktop app ("grimoire" aesthetic). Read `guidelines/docs/coven-design-language.md` before composing larger layouts — it defines the density scale, elevation rules, and voice.

## Setup

- **No provider is required** for most components — tokens are global CSS custom properties from `styles.css`. The default theme is **coven dark**: page background is `var(--background)` (near-black lavender). Always place compositions on `var(--background)` — components on a white page look broken.
- Theme switching: set `data-theme` (21 palettes, default `coven`) and `data-mode` (`dark` | `light`) on `<html>`.
- `ConfirmProvider` (+ its `useConfirm` hook) wraps the app when you need confirm dialogs; `LiveRegionProvider` (+ `useAnnouncer`) wraps the app for screen-reader announcements. Both render children unchanged.

## Styling idiom — tokens first, never hardcoded colors

Every color must come from a token (the app ships 21 themes × 2 modes; literals break them):

- Surfaces: `--background`, `--card`, `--popover`; semantic aliases `--bg-base`, `--bg-raised`, `--bg-elevated`, `--bg-hover`, `--bg-panel`
- Text: `--text-primary`, `--text-secondary`, `--text-muted`; `--foreground`
- Borders: `--border-hairline` (decorative), `--border-strong` (interactive, ≥3:1), `--border`
- Accent: `--accent-presence` (lavender presence — status dots, familiar marks; NOT a CTA color); `--primary` for primary actions
- State: `--color-success` / `--color-warning` / `--color-danger` / `--color-info` (+ `-soft` fills)
- Radii: `--radius-control` (8px), `--radius-card` (12px), `--radius-panel` (16px), `--radius-pill` (999px — the signature shape for chips, avatars, badges)
- Spacing `--space-1`…`--space-10` (4px grid); type scale `--text-2xs`…`--text-display` (13px body baseline — density is a feature); motion `--duration-fast/base/slow` + `--ease-standard/emphasized/decelerate`; focus `--ring-focus`
- Tint recipe for state fills: `color-mix(in oklch, var(--color-danger) 14%, transparent)` fill with a 30–45% border of the same token — never a second hue.

Tailwind v4 utilities are compiled into the stylesheet — the app's idiom is arbitrary-value classes over tokens, e.g. `className="flex gap-2 rounded-lg border border-[var(--border-hairline)] bg-[var(--bg-raised)] px-3 py-2 text-[var(--text-primary)]"`.

Fonts: `var(--font-eb-garamond)` display serif (heroes), `var(--font-inter)` body/UI, `var(--font-jetbrains-mono)` code/labels.

## Where the truth lives

- `styles.css` → imports `_ds_bundle.css`: all token definitions, the `.ui-*` component styles, and compiled utilities. Grep it before inventing a class.
- `guidelines/docs/coven-design-language.md`: density, elevation-via-lightness, the pill, disclosure ladder, chrome budget (≤3 visible actions + overflow), voice.
- Per component: `components/<group>/<Name>/<Name>.prompt.md` (usage + examples) and `<Name>.d.ts` (props).
Comment on lines +30 to +32

## Icons

Use the `Icon` component with bare Phosphor names from the curated union (~290 names): `<Icon name="sparkle" width={14} />`. Common: `sparkle`, `plus`, `magnifying-glass`, `caret-down`, `gear-six`, `dots-three`, `trash`, `warning`, `check`, `x`, `cat`, `folder-open`. Buttons take `leadingIcon`/`trailingIcon` name props directly.

## Idiomatic example

```tsx
import { Button, EmptyState } from "coven-cave";

<div style={{ background: "var(--background)", padding: 24, borderRadius: "var(--radius-card)" }}>
<EmptyState
icon="cat"
headline="No familiars yet"
subtitle="Summon your first familiar to start delegating work."
actions={<Button variant="primary" leadingIcon="sparkle">Summon familiar</Button>}
/>
</div>
```

Voice: terse, sentence case, contractions; domain nouns everywhere (familiar, coven, summon, grimoire, ward) — one flourish per surface, then back to utility.
40 changes: 40 additions & 0 deletions .design-sync/fonts.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/* design-sync authored: font stacks for the Claude Design bundle.
In the app these CSS variables are injected at runtime by next/font
(src/app/fonts.ts); previews and designs have no next/font, so the
canonical faces load from Google Fonts and every --font-* variable the
stylesheet references is defined here with a safe fallback stack.
Canonical trio (docs/coven-design-language.md): EB Garamond (display),
Inter (body/UI), JetBrains Mono (code/labels). */
@import url("https://fonts.googleapis.com/css2?family=EB+Garamond:ital,wght@0,400..800;1,400..800&family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Plus+Jakarta+Sans:ital,wght@0,200..800;1,200..800&family=IBM+Plex+Mono:wght@400;500;600;700&display=swap");

:root {
/* canonical trio — actually loaded above */
--font-eb-garamond: "EB Garamond", Georgia, serif;
--font-inter: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-jetbrains-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

/* catalog faces — user-selectable in the app; previews fall back to
system stacks unless the family is installed locally */
--font-geist-sans: "Geist", -apple-system, BlinkMacSystemFont, sans-serif;
--font-geist-mono: "Geist Mono", ui-monospace, Menlo, monospace;
--font-fredoka: "Fredoka", -apple-system, sans-serif;
--font-instrument-serif: "Instrument Serif", Georgia, serif;
--font-fraunces: "Fraunces", Georgia, serif;
--font-roboto: "Roboto", -apple-system, sans-serif;
--font-open-sans: "Open Sans", -apple-system, sans-serif;
--font-lato: "Lato", -apple-system, sans-serif;
--font-source-sans-3: "Source Sans 3", -apple-system, sans-serif;
--font-noto-sans: "Noto Sans", -apple-system, sans-serif;
--font-ibm-plex-sans: "IBM Plex Sans", -apple-system, sans-serif;
--font-work-sans: "Work Sans", -apple-system, sans-serif;
--font-dm-sans: "DM Sans", -apple-system, sans-serif;
--font-manrope: "Manrope", -apple-system, sans-serif;
--font-figtree: "Figtree", -apple-system, sans-serif;
--font-public-sans: "Public Sans", -apple-system, sans-serif;
--font-fira-code: "Fira Code", ui-monospace, Menlo, monospace;
--font-source-code-pro: "Source Code Pro", ui-monospace, Menlo, monospace;
--font-ibm-plex-mono: "IBM Plex Mono", ui-monospace, Menlo, monospace;
--font-roboto-mono: "Roboto Mono", ui-monospace, Menlo, monospace;
--font-space-mono: "Space Mono", ui-monospace, Menlo, monospace;
--font-inconsolata: "Inconsolata", ui-monospace, Menlo, monospace;
}
14 changes: 14 additions & 0 deletions .design-sync/icon-bootstrap.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// design-sync bundle bootstrap: the app's <Icon name="cat"> uses BARE names,
// but the subset registers under prefix "ph" — in the standalone bundle those
// bare lookups miss (the app has its own runtime affordance). Re-register the
// same icon data with no prefix so iconify's simple-name path stores and
// resolves every name symmetrically ("cat" → '' storage, "caret-down" →
// dash-split 'caret' storage — both sides of the lookup use the same parser).
import { addCollection } from "@iconify/react";
import phSubset from "../src/lib/ph-icons-subset.json";

addCollection({ ...(phSubset as Record<string, unknown>), prefix: "" } as Parameters<
typeof addCollection
>[0]);

export const CovenIconBootstrap = true;
38 changes: 38 additions & 0 deletions .design-sync/previews/AiToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { useState } from "react";
import { AiToggle } from "coven-cave";

function Surface({ children }: { children: React.ReactNode }) {
return (
<div
style={{
background: "var(--background)",
padding: 20,
borderRadius: "var(--radius-card)",
display: "flex",
alignItems: "center",
gap: 12,
flexWrap: "wrap",
}}
>
{children}
</div>
);
}

export const Manual = () => {
const [mode, setMode] = useState<"manual" | "agent">("manual");
return (
<Surface>
<AiToggle mode={mode} onChange={setMode} />
</Surface>
);
};

export const Agent = () => {
const [mode, setMode] = useState<"manual" | "agent">("agent");
return (
<Surface>
<AiToggle mode={mode} onChange={setMode} title="Familiar handles triage" />
</Surface>
);
};
Loading
Loading