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
3 changes: 3 additions & 0 deletions web/.design-sync/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.cache/
learnings/
node_modules
72 changes: 72 additions & 0 deletions web/.design-sync/NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# design-sync notes — Web Sequence (Drafting Table)

Repo-specific gotchas for syncing `web/src/ui` (the "Drafting Table" design system)
to the "Web Sequence Design System" claude.ai/design project.

## Shape & build
- **Package shape, synth-entry.** `web/` is a Vite *app*, not a published library — no
`main`/`module`/`exports`, no library `dist/` with `.d.ts`. The converter synthesizes the
entry from `srcDir: src/ui` (the barrel `src/ui/index.ts`). Run `package-build.mjs` WITHOUT
`--entry`. `--node-modules ./node_modules` (web/'s own; react/radix resolve there).
- React 19 + Radix UI primitives + Tailwind 3. `@types/react` must be installed in `.ds-sync`.

## CSS (Tailwind) — must be recompiled on re-sync
- Component classes are Tailwind utilities; `tailwind.config.js` is the source of truth. The
converter scrapes a *static* stylesheet, so we precompile one into `cssEntry`:
```sh
cd web
npx tailwindcss -i src/styles/globals.css -o .design-sync/.cache/ds-tailwind.css
# then prepend the Google-Fonts @import (brand fonts load at runtime, like index.html):
```
The `.design-sync/build-css.sh` helper does both. Re-run it before every build so new
utility classes used by authored previews are included (extend its content glob to
`.design-sync/previews` once previews exist).
- **Fonts: Google Fonts at runtime** (Hanken Grotesk / IBM Plex Mono / Instrument Serif), via
a `<link>` in `index.html`. Not shipped as woff2. We inject the same `@import url(fonts.googleapis…)`
at the top of the compiled CSS → `[FONT_REMOTE]` (loads at runtime, no woff2 to ship).
`runtimeFontPrefixes` is set as a backstop so `[FONT_MISSING]` stays quiet.

## Preview authoring — calibration learnings (solo: Button/IconButton/Dialog)
- **Dark-surface DS → wrap preview content in a dark ink panel** (`background:#10141B`, padding,
radius). The grading capture sheet uses a WHITE bg, so `surface="dark"` controls (muted
neutrals: ondark-muted icons, ghost buttons) render as faint gray and grade poorly unless
they sit on the ink panel they're designed for. `surface="light"` variants get a warm paper
panel (`#FAF7F1`). Every dark-surface preview should follow this.
- Previews import from `'web-sequence-web'` (the converter aliases it to `window.DraftingTable`).
esbuild auto-JSX runtime — no `import React` needed; `React.FC`/`React.CSSProperties` type
annotations are fine (erased), the IDE's "Cannot find React" warnings are noise.
- Overlay components (Dialog, and likely Popover/Tooltip/Menu/Select open states) need
`cfg.overrides.<Name>: {"cardMode":"single","viewport":"WxH"}` and render open via Radix
`defaultOpen`/`open` so the floated content shows inside the card.
- Use realistic copy from the app (Save changes / Delete diagram / Run diagram), never foo/bar.

## Per-component authoring notes (folded from the wave fan-out)
- **TextInput / Textarea**: thin native wrappers; `surface` prop (default dark) + native attrs.
Uncontrolled `defaultValue` works in previews (onChange is the raw DOM event).
- **SearchInput**: NOT a thin wrapper — `onChange(value: string)` is REQUIRED and value-style
(string, not event); `value` is controlled. A static `value="…"` + a no-op `onChange` renders
the populated state (with the clear ×). `style`/rest spread onto the inner `<input>`.
- **Switch**: on/off via `defaultChecked` (checked = cobalt fill); compose as labelled rows
(mirrors SettingsModal's SwitchRow) on the ink panel.
- **Select / Menu / Popover** (overlays): render OPEN via Radix `defaultOpen` on the Root; the
orchestrator-set `cfg.overrides.{…}` (cardMode single + viewport) lands the portaled content
in the card. `SelectContent`/`MenuContent` are dark-ink; `PopoverContent` is light-paper (it
brings its own surface even off a dark trigger). `MenuItem tone="danger"` = red Delete.
- **Tooltip** (the context-identity trap): the DS `Tooltip` wrapper self-provides a Radix Root
with NO open-control prop, and the barrel exports only `Tooltip`+`TooltipProvider` (no raw
Root/Trigger/Content). To show it open, the preview imports raw `@radix-ui/react-tooltip` AND
supplies its OWN `<RadixTooltip.Provider>` — the bundle's `cfg.provider` TooltipProvider is a
DIFFERENT module copy with a distinct React context, so a raw Root can't see it (renders blank
+ throws "must be used within TooltipProvider"). **General rule**: any overlay whose package
wrapper self-provides but exposes no open prop → preview supplies its own matching Radix Provider.

## Known render warns (re-sync should treat these as expected, not new)
- `[RENDER_THIN]` on **BrandLogo** — false positive: the logo is a pure SVG with no text nodes.
Grade `good` whenever the mark renders.

## Re-sync risks
- `.design-sync/.cache/ds-tailwind.css` is gitignored and regenerated — re-sync MUST run
`build-css.sh` first or the bundle ships unstyled.
- Compound exports (DialogTrigger, SelectItem, MenuItem…) are PascalCase and may be discovered
as separate components — they're real API parts, not standalone cards; previews compose them
inside their parent (Dialog, Select, Menu).
12 changes: 12 additions & 0 deletions web/.design-sync/build-css.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#!/bin/sh
# Compile the Drafting Table Tailwind CSS into the design-sync bundle's cssEntry.
# Content covers src/** (the DS + app usage) AND authored previews.
set -e
cd "$(dirname "$0")/.." # -> web/
OUT=.design-sync/.cache/ds-tailwind.css
mkdir -p .design-sync/.cache
npx tailwindcss -i src/styles/globals.css -o "$OUT" \
--content './index.html,./src/**/*.{ts,tsx},./.design-sync/previews/**/*.{ts,tsx}'
FONT="@import url('https://fonts.googleapis.com/css2?family=Hanken+Grotesk:wght@400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&family=Instrument+Serif:ital@0;1&display=swap');"
{ echo "$FONT"; cat "$OUT"; } > "$OUT.tmp" && mv "$OUT.tmp" "$OUT"
echo "wrote $OUT ($(wc -c <"$OUT") bytes)"
34 changes: 34 additions & 0 deletions web/.design-sync/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"projectId": "ffa534fb-665d-47a6-af0d-7dbbc27d5e88",
"shape": "package",
"pkg": "web-sequence-web",
"globalName": "DraftingTable",
"srcDir": "src/ui",
"tsconfig": "tsconfig.json",
"cssEntry": ".design-sync/.cache/ds-tailwind.css",
"readmeHeader": ".design-sync/conventions.md",
"runtimeFontPrefixes": ["Hanken Grotesk", "IBM Plex Mono", "Instrument Serif"],
"componentSrcMap": {
"Button": "src/ui/Button.tsx",
"IconButton": "src/ui/IconButton.tsx",
"BrandLogo": "src/ui/BrandLogo.tsx",
"Dialog": "src/ui/Dialog.tsx",
"TextInput": "src/ui/TextInput.tsx",
"Textarea": "src/ui/Textarea.tsx",
"Switch": "src/ui/Switch.tsx",
"Select": "src/ui/Select.tsx",
"SearchInput": "src/ui/SearchInput.tsx",
"Popover": "src/ui/Popover.tsx",
"Tooltip": "src/ui/Tooltip.tsx",
"Menu": "src/ui/Menu.tsx"
},
"provider": { "component": "TooltipProvider" },
"overrides": {
"Dialog": { "cardMode": "single", "viewport": "480x360" },
"Select": { "cardMode": "single", "viewport": "360x320" },
"Popover": { "cardMode": "single", "viewport": "400x300" },
"Menu": { "cardMode": "single", "viewport": "360x340" },
"Tooltip": { "cardMode": "single", "viewport": "360x200" },
"Textarea": { "cardMode": "column" }
}
}
57 changes: 57 additions & 0 deletions web/.design-sync/conventions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Drafting Table — how to build with this design system

The ZenUML Web Sequence UI kit. Compose from the real components on
`window.DraftingTable.*` (Button, IconButton, Dialog, TextInput, Textarea, Switch,
Select, SearchInput, Popover, Tooltip, Menu, BrandLogo) and style your own layout
glue with the Tailwind utilities below. Two fixed surfaces, one cobalt accent — never
a generic gray/blue palette.

## Setup
- Load `styles.css` — it `@import`s the tokens, the component CSS (`_ds_bundle.css`),
and the brand fonts (Hanken Grotesk / IBM Plex Mono / Instrument Serif from Google
Fonts). Without it everything renders unstyled in a fallback font.
- **No global wrapper is needed** for most components — they style themselves. The one
exception: anything using `Tooltip` must be inside `<TooltipProvider>` (export it from
`window.DraftingTable.TooltipProvider`, wrap once near the root).

## The two surfaces (the core idea)
Interactive components are **surface-aware** via a `surface` prop, not a runtime theme:
- `surface="dark"` (the **default**) — the **ink** chrome: charcoal-with-blue-undertone
editor/header/toolbar/menus. Most of the app lives here.
- `surface="light"` — the warm **paper** surface (the diagram canvas / some popovers).
Pick the surface to match the panel you place the component on. `Button`, `IconButton`,
`TextInput`, `Textarea`, `SearchInput` all take `surface`; `SelectContent`/`MenuContent`
are dark ink, `PopoverContent` is light paper by design.

## Styling idiom — semantic Tailwind utilities
Utility classes from a custom palette (NOT Tailwind's default gray/blue). Use real names:

| role | classes |
|---|---|
| Dark surfaces (ink) | `bg-ink-950` (backdrop) · `bg-ink-900` (rail) · `bg-ink-800` (panel) · `bg-ink-700` (raised) · `border-ink-line` |
| Light surfaces (paper) | `bg-paper-50` · `bg-paper-100` · `bg-paper-200` · `border-paper-line` |
| Accent — the one cobalt signal | `bg-accent` · `bg-accent-press` (pressed) · `text-accent` (fills/rings); for accent TEXT on dark use `text-accent-onDark` (AA-safe) |
| Text on dark | `text-ondark-strong` · `text-ondark-muted` · `text-ondark-faint` |
| Text on light | `text-onlight-strong` · `text-onlight-muted` · `text-onlight-faint` |
| Intent | `text-danger` (use `text-danger-strong` for danger TEXT on light) · `text-ok` · `text-signal-amber` (sparingly) |
| Type | `font-sans` (Hanken Grotesk, default) · `font-mono` (IBM Plex Mono, code/DSL) · `font-serif` (Instrument Serif, display titles like the Dialog heading) |
| Shape | `rounded` (7px) · `rounded-lg` (11px) · `shadow-pop` / `shadow-pop-dark` for lifted surfaces |

Rules of thumb: dark panels pair `bg-ink-*` with `text-ondark-*`; light panels pair
`bg-paper-*` with `text-onlight-*`. Reach for `bg-accent` once per view, not everywhere.

## Where the real truth lives
- `styles.css` and its `@import` closure (`_ds_bundle.css`, tokens) — the compiled palette.
- Each component's `components/<group>/<Name>/<Name>.d.ts` (its prop API) and
`<Name>.prompt.md` (usage). Read those before composing a component you're unsure of.

## One idiomatic snippet
```tsx
// A dark toolbar with the primary action + an icon control, on the ink surface.
<div className="flex items-center gap-2 bg-ink-800 border border-ink-line rounded-lg p-2">
<Button variant="primary">Run diagram</Button>
<Button variant="ghost">Export</Button>
<IconButton aria-label="Add page"><PlusIcon /></IconButton>
</div>
```
`variant` on Button: `primary` (cobalt) · `subtle` · `ghost` · `danger`. Sizes `sm` / `md`.
30 changes: 30 additions & 0 deletions web/.design-sync/previews/BrandLogo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { BrandLogo } from 'web-sequence-web';

// The official ZenUML mark — a self-contained #2E94D4 rounded-square SVG that
// carries its own background, so it reads on any surface. Sized via a wrapping
// div width (the SVG fills 100% of its box at a 300×300 viewBox).
// NOTE: BrandLogo trips a [RENDER_THIN] warn — it's an SVG with no text nodes,
// which is a FALSE POSITIVE for a logo. Grade `good` whenever the mark renders.

// The logo at the three sizes the app uses it: brand wordmark (48), header /
// menu avatar (30), and a compact favicon-scale chip (20) — on neutral paper.
export function Sizes() {
return (
<div style={{ background: '#FAF7F1', padding: 16, borderRadius: 8, display: 'flex', gap: 16, alignItems: 'flex-end' }}>
<div style={{ width: 48 }}><BrandLogo /></div>
<div style={{ width: 30 }}><BrandLogo /></div>
<div style={{ width: 20 }}><BrandLogo /></div>
</div>
);
}

// In context: the mark sitting in the dark app header next to the product name,
// exactly as AppMenu / HomeView place it (className-sized to 30×30).
export function InHeader() {
return (
<div style={{ background: '#10141B', padding: 16, borderRadius: 8, display: 'flex', gap: 10, alignItems: 'center' }}>
<BrandLogo className="h-[30px] w-[30px] shrink-0" />
<span style={{ color: '#E7ECF3', fontSize: 16, fontWeight: 600, letterSpacing: '-0.01em' }}>ZenUML</span>
</div>
);
}
51 changes: 51 additions & 0 deletions web/.design-sync/previews/Button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Button } from 'web-sequence-web';

// Drafting Table is a dark-surface DS: `dark` (default) controls live on the ink
// chrome. Preview them on that ink panel so the quiet variants (ghost/subtle) read
// with proper contrast; the `light` variant gets the warm paper panel.
const Ink: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', background: '#10141B', padding: 16, borderRadius: 8 }}>
{children}
</div>
);

// Intent-based variants on the dark `ink` chrome (header/toolbar surface).
export function Variants() {
return (
<Ink>
<Button variant="primary">Save changes</Button>
<Button variant="subtle">Cancel</Button>
<Button variant="ghost">Dismiss</Button>
<Button variant="danger">Delete page</Button>
</Ink>
);
}

// Two sizes for toolbars (sm) vs. dialogs/forms (md).
export function Sizes() {
return (
<Ink>
<Button variant="primary" size="sm">Run</Button>
<Button variant="primary" size="md">Run diagram</Button>
<Button variant="subtle" size="sm">Export</Button>
<Button variant="subtle" size="md">Export as PNG</Button>
</Ink>
);
}

// Disabled state (dark) + the light-paper surface variant (used inside modals/menus).
export function StatesAndSurface() {
return (
<div style={{ display: 'flex', flexDirection: 'column', gap: 12 }}>
<Ink>
<Button variant="primary" disabled>Saving…</Button>
<Button variant="subtle" disabled>Cancel</Button>
</Ink>
<div style={{ display: 'flex', gap: 12, alignItems: 'center', flexWrap: 'wrap', background: '#FAF7F1', padding: 16, borderRadius: 8 }}>
<Button variant="primary" surface="light">Confirm</Button>
<Button variant="subtle" surface="light">Cancel</Button>
<Button variant="danger" surface="light">Remove</Button>
</div>
</div>
);
}
20 changes: 20 additions & 0 deletions web/.design-sync/previews/Dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { Dialog, DialogContent, Button } from 'web-sequence-web';

// Modal shell over Radix Dialog on the dark ink surface. Rendered open (`defaultOpen`)
// so the card shows the lifted dialog; compose DialogContent with a title, optional
// description, and your own footer actions. (Ported from the app's ConfirmDialog.)
export function ConfirmDestructive() {
return (
<Dialog defaultOpen>
<DialogContent
title="Delete diagram?"
description="“Order processing flow” will be permanently removed. This cannot be undone."
>
<div style={{ marginTop: 20, display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
<Button variant="subtle">Cancel</Button>
<Button variant="danger">Delete diagram</Button>
</div>
</DialogContent>
</Dialog>
);
}
56 changes: 56 additions & 0 deletions web/.design-sync/previews/IconButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { IconButton } from 'web-sequence-web';

// Drafting Table is a dark-surface DS: its `dark` controls are muted neutrals meant
// to sit on the ink chrome. Preview them on that ink panel (else the icons read as
// faint gray on a white card). The `light` variant gets the warm paper panel.
const Ink: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div style={{ display: 'flex', gap: 10, alignItems: 'center', background: '#10141B', padding: 12, borderRadius: 8 }}>
{children}
</div>
);
const Paper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<div style={{ display: 'flex', gap: 10, alignItems: 'center', background: '#FAF7F1', padding: 12, borderRadius: 8 }}>
{children}
</div>
);

const Close = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M4 4l8 8M12 4l-8 8" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);
const Plus = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" aria-hidden="true">
<path d="M8 3v10M3 8h10" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
</svg>
);

// Icon-only controls for toolbars and tab affordances. aria-label is required.
export function Toolbar() {
return (
<Ink>
<IconButton aria-label="Add page"><Plus /></IconButton>
<IconButton aria-label="Close"><Close /></IconButton>
</Ink>
);
}

export function Sizes() {
return (
<Ink>
<IconButton size="sm" aria-label="Add page (small)"><Plus /></IconButton>
<IconButton size="md" aria-label="Add page (medium)"><Plus /></IconButton>
</Ink>
);
}

// The light-paper surface variant + a disabled control.
export function SurfaceAndDisabled() {
return (
<Paper>
<IconButton surface="light" aria-label="Close on light"><Close /></IconButton>
<IconButton surface="light" aria-label="Add on light"><Plus /></IconButton>
<IconButton surface="light" aria-label="Close (disabled)" disabled><Close /></IconButton>
</Paper>
);
}
54 changes: 54 additions & 0 deletions web/.design-sync/previews/Menu.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
Menu,
MenuTrigger,
MenuContent,
MenuItem,
MenuLabel,
MenuSeparator,
} from 'web-sequence-web';

// Design-system dropdown Menu over Radix DropdownMenu. Menus are DARK (ink) — they
// drop from the dark chrome (header, diagram-card kebab) and bring their own surface.
// Rendered OPEN (`defaultOpen`) so the card shows the floated menu items, not just
// the trigger. Content + items copied from the DiagramCard kebab menu (Duplicate /
// Export / a separator / a destructive Delete), the canonical realistic item list.
const inkPanel: React.CSSProperties = {
background: '#10141B',
padding: 16,
borderRadius: 8,
};

const trigger: React.CSSProperties = {
display: 'inline-flex',
alignItems: 'center',
gap: 6,
height: 32,
padding: '0 10px',
borderRadius: 6,
fontSize: 13,
color: '#E7ECF3',
background: 'rgba(255,255,255,0.06)',
border: '1px solid rgba(255,255,255,0.08)',
};

export function DiagramActions() {
return (
<div style={inkPanel}>
<Menu defaultOpen>
<MenuTrigger asChild>
<button type="button" style={trigger} aria-label="Diagram options">
Order processing flow ▾
</button>
</MenuTrigger>
<MenuContent align="start" sideOffset={6}>
<MenuLabel>Diagram</MenuLabel>
<MenuItem>Rename</MenuItem>
<MenuItem>Duplicate</MenuItem>
<MenuItem>Export as HTML</MenuItem>
<MenuSeparator />
<MenuItem tone="danger">Delete</MenuItem>
</MenuContent>
</Menu>
</div>
);
}
Loading
Loading