diff --git a/.fallowrc.json b/.fallowrc.json index 785ae90..58c70ac 100644 --- a/.fallowrc.json +++ b/.fallowrc.json @@ -9,7 +9,8 @@ "**/__snapshots__/**", "**/turbo/generators/templates/**", "**/*.generated.ts", - "**/*.d.ts" + "**/*.d.ts", + "docs/product/reference/**" ], "dynamicallyLoaded": [ "packages/**/__factories__/**", diff --git a/.prettierignore b/.prettierignore index d230ed6..d42c1c8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,2 +1,5 @@ # Generated compliance artifacts — do not reformat compliance/*.yml + +# Design reference material (ADR-029) — committed byte-for-byte, never reformat +docs/product/reference/ diff --git a/docs/product/reference/README.md b/docs/product/reference/README.md new file mode 100644 index 0000000..8f8bce6 --- /dev/null +++ b/docs/product/reference/README.md @@ -0,0 +1,22 @@ +# CODING AGENTS: READ THIS FIRST + +This is a **handoff bundle** from Claude Design (claude.ai/design). + +A user mocked up designs in HTML/CSS/JS using an AI design tool, then exported this bundle so a coding agent can implement the designs for real. + +## What you should do — IMPORTANT + +**Read `app-prototype-development/project/Veect.dc.html` in full.** The user had this file open when they triggered the handoff, so it's almost certainly the primary design they want built. Read it top to bottom — don't skim. Then **follow its imports**: open every file it pulls in (shared components, CSS, scripts) so you understand how the pieces fit together before you start implementing. + +**If anything is ambiguous, ask the user to confirm before you start implementing.** It's much cheaper to clarify scope up front than to build the wrong thing. + +## About the design files + +The design medium is **HTML/CSS/JS** — these are prototypes, not production code. Your job is to **recreate them pixel-perfectly** in whatever technology makes sense for the target codebase (React, Vue, native, whatever fits). Match the visual output; don't copy the prototype's internal structure unless it happens to fit. + +**Don't render these files in a browser or take screenshots unless the user asks you to.** Everything you need — dimensions, colors, layout rules — is spelled out in the source. Read the HTML and CSS directly; a screenshot won't tell you anything they don't. + +## Bundle contents + +- `app-prototype-development/README.md` — this file +- `app-prototype-development/project/` — the `App prototype development` project files (HTML prototypes, assets, components) diff --git a/docs/product/reference/project/.thumbnail b/docs/product/reference/project/.thumbnail new file mode 100644 index 0000000..d9ac15f Binary files /dev/null and b/docs/product/reference/project/.thumbnail differ diff --git a/docs/product/reference/project/VEECT-SPEC.md b/docs/product/reference/project/VEECT-SPEC.md new file mode 100644 index 0000000..0091698 --- /dev/null +++ b/docs/product/reference/project/VEECT-SPEC.md @@ -0,0 +1,251 @@ +# Veect — Product & Design Specification + +_v1.0 · July 2026 · covers `Veect.dc.html` (interactive prototype), `Veect Landing.dc.html`, and `veect-codebase/` (React 18 · TypeScript strict · Tailwind · shadcn-style)_ + +--- + +## 1. Product + +**Veect is a design-system-native canvas.** A designer brings their own tokens and +components; Veect composes screens from *only* that system and exports production React +that imports their real library. The three invariants everything hangs on: + +1. **One runtime** — canvas, preview, isolation and export render the same tree through + the same renderer. No "roughly how it'll look." +2. **0 unregistered elements** — AI output is whitelist-sanitized against the component + registry; anything outside it is refused, never faked. +3. **Deterministic code** — same tree → same code, every time. Export imports the + customer's library (`@solstice/ui` in the demo), never inlined clones. + +Demo fiction: the customer is **Solstice** (warm team-scheduling brand), with designer +Maya (owner) and design engineer Devon (reviewer, owns the merge veto). + +--- + +## 2. Brand & design language + +### 2.1 Logo +Typographic wordmark: lowercase **`veect`**, Instrument Sans 600, letter-spacing −0.035em, +line-height 1, closed by a **square ink terminal** (the "vector point") sized ≈ 0.28 × font +size, baseline-aligned, `currentColor`. No pictorial mark. Codebase atom: `Logo size={n}`. + +### 2.2 Color — chrome tokens +Color belongs to the customer's canvas; the instrument is monochrome. The accent IS the ink. + +| Token | Light (default file value) | Dark | +|---|---|---| +| `--bg` / `--panel` | `#FCFCFB` | `#0E0E0D` | +| `--raised` / `--chip` | `#FFFFFF` / `#F4F3F0` | `#161615` | +| `--line` / `--line2` | `#E7E6E2` / `#CFCEC8` | `#262624` / `#3A3A37` | +| `--t1` / `--t2` / `--t3` | `#161513` / `#63615B` / `#9A978F` | `#F4F4F2` / `#A6A6A1` / `#73736E` | +| `--iris` (accent = ink) | `#161513` | `#F4F4F2` | +| `--iris-dim` | `rgba(22,21,19,.06)` | `rgba(244,244,242,.09)` | +| `--btn-ink` (text on accent) | `#FFFFFF` | `#0E0E0D` | +| `--warn` / `--err` / `--live` | `#9A7B3F` / `#B05E5E` / `#5B7F5E` | `#C2A36B` / `#C97070` / `#8BA88E` | + +Amber (`--warn`) is reserved for AI-scope affordances (pick outline, scope chips, draw +strokes) and the unmapped-component warning. The prototype ships **dark by default** +(user preference); the codebase mirrors this (`index.html data-theme="dark"`). + +### 2.3 Surfaces & elevation +Flat. Radius **2px everywhere** in chrome (customer canvas keeps its own `radius-md` +token). No floating shadows — separation is a hairline: popovers/menus use +`box-shadow: 0 0 0 1px var(--line2)`. Canvas frames sit on the board with a 1px +`rgba(0,0,0,.14)` ring. + +### 2.4 Typography +- **UI**: Instrument Sans 400/500/600 — 13px base, 11–12.5px controls. +- **Labels**: Fragment Mono, 9–11px, uppercase, letter-spacing .06–.14em — all section + headers, metadata, and "engraved" captions. +- **Customer content**: the system's own font (demo: Bricolage Grotesque) — never used + for chrome. +- Scale discipline: chrome text never below 8px (activity-bar labels), canvas display + type responsive per view (§6). + +### 2.5 Iconography +Hand-drawn 16-grid SVG, 1.5px stroke, round caps, `currentColor`. Set: chat (spark), +layers, library, tokens, history, inspect (sliders), polish (spark-4), code (chevrons), +pick (crosshair), draw (pen), eye / eye-off, mic. No icon fonts, no emoji. + +### 2.6 Motion +120–450ms, `cubic-bezier(.2,.7,.2,1)`. Keyframes: `riseIn`, `popIn`, `fadeIn`, +`lineFlash` (code sync), `nodePulse` (token ripple). Everything respects +`prefers-reduced-motion`. + +### 2.7 Voice +Plainspoken, no hype, no emoji. Refusals teach ("You don't have a component for this +yet"), fixes explain the *why* in the user's own tokens. + +--- + +## 3. Screens + +| Screen | Purpose | Key elements | +|---|---|---| +| **Sign in** | design-partner entry | email → straight through; SSO stubs; provisioning note | +| **Home** | projects + systems | project grid (1 live + placeholders), search, Solstice system card (ramp, open tokens, re-import), "bring a design system" | +| **Onboarding** | the re-theme reveal | two paths: paste tokens (parse → contrast checks) or 5-token quick path (brand, neutrals, radius, type, spacing) with live mini-kit; ends "Bring it to life →" | +| **Editor** | the canvas (§4–5) | | +| **Settings** | system + governance | token source card, component mappings ("merge bar"), craft standard picker, export defaults, plan, delete | +| **Profile** | account | identity card, theme preference, workspace members (Maya/Devon), keyboard map, sign out | +| **System update** | US-6 re-import | paste → diff (CHANGED/ADDED/REMOVED + swatches) → impact line → apply-with-undo → ripple | +| **Landing** | marketing (§10) | | + +Navigation: project dropdown (top-left) is the hub — all projects, new project, export, +copy TSX, import tokens, upload component, settings, profile, onboarding demo. Escape and +outside-click dismiss. Avatar → Profile everywhere. + +--- + +## 4. Editor anatomy + +### 4.1 Top bar (36px) +Wordmark · project dropdown · `✓ saved` · **view switcher** (centered: Desktop / Tablet / +Mobile / ⧉ All) · Preview · **Export** (primary) · theme toggle ◐ · avatar. + +### 4.2 Activity bar (44px, fixed left) +Toggleable icon+label buttons, grouped by dividers: +`✦ Chat` │ `Layers · Library · Tokens · History` │ `Inspect · Polish` │ `Code`. +Click = open that panel/tab; click again = collapse. Active = `--iris-dim` chip. At +overlay widths (<1000px) panels are mutually exclusive and float over the board. + +### 4.3 Panels (resizable via edge-drag; « » collapse chevrons) +- **Chat** (284px) — vertical AI thread (§7): bubbles, proposal Accept/Discard inline, + refusal actions, model picker (haiku/sonnet), image attachments, scope chip. +- **Board rail** (216px) — LAYERS: collapsible tree (caret-expand, double-click reveal), + per-frame **variant chips** (§6); LIBRARY: 10-component base kit with mapping badges, + isolation ◉, add +, drag-to-canvas, patterns, custom `.tsx/.jsx` upload; TOKENS: brand + ramp + curated swatches + hex, radius stepper, type picker, neutrals, spacing base — + every change ripples live (`nodePulse`); HISTORY: labelled steps, restore cursor (§8). +- **Inspector** (244px) — selection card, ✦ Edit-with-AI (scoped), content/variant/weight/ + tone/size controls, gap/pad steppers (system-scale steps), Tailwind utilities + (collapsible; w/h/max-w/shadow/opacity selects + custom string), token bindings, + FRAME settings (name, width ±80, **active view** line). Polish tab: §7.3. +- **Code** (380px, dark `#161311` always) — file segs (`{Frame}.tsx` / `tokens.css`), + line-numbered TSX with click-line→select-node sync and `lineFlash` on change; **edit + mode** (toggle → syntax-highlighted textarea → apply parses back via the generated + grammar, line-referenced errors); footer `prettier ✓ tsc ✓`. +- **Isolation** (over board) — variant × state grid per component on a paper stage, real + runtime, Esc exits. + +### 4.4 Board +Infinite surface: scroll pans, ⌘/ctrl-scroll zooms at cursor, two-finger pinch, drag +empty board pans, background click deselects. Zoom cluster (− % + · fit · + frame · +board backdrop toggle). Frames render as **view clusters** (§6) with a draggable name +tab (`Home · 34 nodes · one codebase`). Selection = ink ring; hover = neutral ring; +AI-target = amber ring. Tool pill (bottom-center): **Pick (V) · Draw (D) · Overlays (O) +· Voice (stub)** — icon buttons with key hints. + +### 4.5 Preview +Full-screen overlay, real hover/focus states, active view's width fills the screen +(mobile/tablet render as a centered device sheet). + +### 4.6 Keyboard +`⌘K` commands · `✦` focus composer · `⌘Z/⇧⌘Z` undo/redo · `⌘0` fit · `⌘1` 100% · +`⌘±` zoom · `V/D/O` tools · `⌫` delete selection · `Esc` dismiss cascade (menu → modes → +refusal → proposal → scope → selection). + +--- + +## 5. Canvas node model + +`board → frame[] → (stack | card | heading | text | button | badge | input | avatar | +image | divider | custom)*`. Layout via `dir/gap/pad/padX/padY/align/justify/maxW/bg` +(4px scale). Content props per type; `tw` bag for curated utility classes. Frames carry +`x/y/w/views/view`. IDs are stable; comment anchors and code-line mapping key off them. + +--- + +## 6. Responsive variants — one tree, three widths + +- Views: **desktop** (frame's own `w`, default 1280) · **tablet 768** · **mobile 390**. +- A frame renders every enabled view side by side (120px gap) — same children, so an + edit anywhere updates every view. Captions (`tablet · 768`) label each block; the + **active view** (drives preview, code, export width) is ink-highlighted. +- Renderer responds to view width: row-stacks wrap ≤900, cards get flex-basis 240, + display type steps down (xl: 54 → 44 → 34px) ≤900/≤480. +- Switcher semantics: Desktop/Tablet/Mobile = show only that view + make it active; + **⧉ All** = all three. Layer chips: label click = select frame + make active (opens + frame settings); eye icon = show/hide that view (min 1). Cluster growth shifts + right-neighbors by the width delta — clusters never collide. + +--- + +## 7. AI, constrained + +### 7.1 Compose +Prompt → system prompt embeds the registry JSON (types + allowed props) + brand voice +rules (one primary CTA per group, 4px spacing, no lorem). Response is sanitized: unknown +type ⇒ **refusal**, numeric props clamped to scale, strings capped. Result stages as a +**dashed ghost** on the active frame — Accept commits (history label `AI compose — +accepted`), Discard drops. + +### 7.2 Scoped edits +Pick tool (V) or Inspector → "✦ Edit with AI": amber target ring + composer scope chip +(`h1 · Scheduling…`), inline bubble at cursor, type-aware quick actions (punchier / +shorter / demote / colorize ×n…). Edit proposals ghost beside the original and swap in +on Accept. + +### 7.3 Polish (the Impeccable pass) +Ranked issues (HIGH/MED/LOW): weak hierarchy (display heading at 400), competing primary +CTAs, off-scale gaps (`gap-[18px]` drift), uneven sibling padding, AA contrast failures +(with measured ratios). Each: title — *why it matters* — one-click fix **in the user's +tokens**. Fix-all; empty state "Impeccable. Nothing to fix." Craft standard selectable +in Settings (Veect default / Impeccable / custom DESIGN.md). + +### 7.4 Refusal protocol +Off-registry ask (3D globe, video, charts…) ⇒ "You don't have a component for this yet" ++ reason + **Compose from primitives** / Not now. Never generic markup. Models: Haiku 4.5 +default, Sonnet 4.5 option; offline heuristics keep demos deterministic. + +--- + +## 8. History + +Every mutation is a labelled step (`Add Button`, `Polish — Two primary actions`, +`View → tablet`, `AI edit — accepted`…). History tab lists steps newest-first with age. +Clicking restores that state and sets a **cursor**: newer steps dim (35%); the next +meaningful edit truncates the dimmed trail. `⌘Z` walks steps; redo restores. + +--- + +## 9. Code generation & round-trip + +| Canvas | Export | Notes | +|---|---|---| +| heading/text | `/

` + Tailwind | size/weight/tone classes, neutral ramp | +| button | ` + + + {{ brandHex }} + +

+ radius + + {{ radiusLabel }} +
+
+ + + +
+ +
+
+ Get started + Docs + + NEW +
+
+ Email +
you@company.com
+
+
+ + Notifications + + your tokens · live +
+
+ + + + +
+
+
+ 0 + UNREGISTERED ELEMENTS
ACROSS A 100-PROMPT EVAL
+
+
+ <10% + LOC CHANGED PRE-MERGE —
THE ENGINEER IS THE JUDGE
+
+
+ <20 min + SIGN-IN TO YOUR FIRST
EXPORTED, ON-BRAND SCREEN
+
+
+
+ +
+
+ WHY IT HOLDS +

Other tools re-invent your UI. Veect is constrained to it.

+
+
+ 01 — YOUR COMPONENTS + The canvas renders the real thing + One runtime backs canvas, preview and export. "Roughly how it'll look" is the lie that makes handoff lossy — Veect never tells it. +
+
+ 02 — YOUR TOKENS + Change once, everywhere + Edit brand-500 and watch every bound component ripple on canvas and in code — the system stays the single source of truth. +
+
+ 03 — REAL IMPORTS + Code an engineer would merge + Export imports your mapped library — import paths, prop names and all. Not inlined clones. That is the whole trust bar. +
+
+
+
+ +
+
+
+ THE GRACEFUL REFUSAL +

When AI can't stay on-system, it says so.

+

Ask for something your registry can't express and Veect refuses — then offers to compose it from your primitives. It never fails open to generic markup. Across a 100-prompt eval: zero unregistered elements.

+
+
+
+ A hero with a spinning 3D globe +
+
+
+ You don't have a component for this yet. + + Compose from primitives + Not now + +
+
+ an error state, elevated to a signature moment +
+
+
+ +
+
+
+
+ Home.tsx + + + in sync +
+
+
import { Button, Card, TextField } from "@solstice/ui";
+
 
+
export default function Home() {
+
return (
+
<main className="flex flex-col bg-white">
+
<Button kind="primary">Start free</Button>
+
</main>
+
);
+
}
+
+
+ prettier ✓tsc --noEmit ✓imports your library +
+
+
+ TRUST IS EARNED IN THE OPEN +

The code is a keystroke away — and it's yours.

+

Nudge a component and the code updates in lockstep. Deterministic output — same tree, same code — that passes Prettier, tsc and your reviewer's patience.

+
+
+
+ +
+
+
+ POLISH — THE IMPECCABLE PASS +

On-brand and genuinely well-designed.

+

One pass critiques spacing, hierarchy and contrast against a craft standard — and fixes them in your tokens, with the why. Try it.

+
+ + {{ item.label }} + +
+
+
+
+ New — shared team calendars + Scheduling that feels like sunlight + Every timezone, handled kindly. +
+ Start free + Book a demo +
+
+
+ + {{ polishNote }} +
+
+
+
+ +
+
+

What you design is what ships.

+ Open the canvas → +
+ + veect + + working name · design-partner MVP + + tokens in · react out · impeccable by default +
+
+
+ + + + + + diff --git a/docs/product/reference/project/Veect.dc.html b/docs/product/reference/project/Veect.dc.html new file mode 100644 index 0000000..de74167 --- /dev/null +++ b/docs/product/reference/project/Veect.dc.html @@ -0,0 +1,4142 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ veect + design-system-native canvas +
+

Connect your repository.

+

Veect reads your components and tokens straight from the source — then composes production React on a branch you can publish. Nothing to paste, nothing to upload.

+
+ + + +
+
+ +
+
+ + + +

Authorize Veect on GitHub

+

Enter this code at github.com to grant access. Veect requests the narrowest scope that still lets it read components and open pull requests.

+
+
+ DEVICE CODE + + Approve in browser → +
+ + waiting for approval… +
+
+
+ +
{{ cxErr }}
+
+ + + +
+
+
+ + + +

Your org hasn't authorized Veect yet

+

Sign-in worked, but solstice/web is behind an organization policy. An owner needs to approve Veect once — then every seat can connect.

+
+
+ You can + Open the org's third-party access settings and approve Veect, or send the request to an admin. +
+
+ Open approval settings → + +
+
+ +
+ + + +

Confirm the host key

+

First connection to git.solstice.dev over SSH. Check this fingerprint matches what your host published before trusting it.

+
+ ED25519 FINGERPRINT + SHA256:kR8Hq3vL0pR2mNbXcZ1wY7tSh6dKfJ4aQeUoIgP5s +
+ + +
+
+
+ + +

Trust this repository?

+

Veect will run {{ cxRepoName }}'s install scripts and dev server on your machine to render real components. Only connect repositories you trust.

+
+ + + + the “without scripts” choice is remembered per repository +
+
+ + +

Connecting {{ cxRepoName }}

+

This runs on your machine and can take a few minutes on first connect. You can leave it running.

+
+ +
+ {{ item.tick }} + {{ item.label }} + {{ item.note }} +
+
+
+ +
{{ cxErr }}
+
+ + +
+
+ +
+ + + +
+
+
+
+ + +

Point Veect at your components

+

No veect.config.json yet — pick the folders to read. This config rides your first published branch.

+
+
+ COMPONENT FOLDERS + + + +
+
+
+ APP DIRECTORY (MONOREPO) + +
+
+ DETECTED + +
+ + {{ item.k }} + + {{ item.v }} +
+
+
+ +
+
+
+ + +

Environment

+

Your preview needs a few keys. They stay on this machine, encrypted — Veect never sends them anywhere.

+
+ +
+ {{ item.key }} + {{ item.src }} + + {{ item.chip }} +
+
+
+
+ + + +
+
+ + +
+ +

Reading your repository…

+
+ +

Your system is live.

+
+ {{ revealStat }} +
+
+
+
Get started
+
Secondary
+
+
+
NEW
+
Filter · 3
+
+
+ Email +
you@company.com
+
+
+
+
MK
+
+ Maya Kade + Product designer +
+
+
+
+
+
+ Overview + Members + Billing +
+
+
+
+
+
+
+ Notifications +
+
+
+
+
+
+ volume — 62 +
+
+ Ag + {{ revealFontNote }} +
+
+ +
+ + ⌘K anywhere · ✦ to compose +
+
+
+ +
+
+
+ + +
+ +
+ veect +
+ + +
+
+
+ +
+ Solstice + workspace · solstice/web · main +
+
+
+ PROJECTS · EACH ON ITS BRANCH + + + + +
+
+ + +
+
+ + + +
+
+
+
+
+ + + + {{ saveLabel }} + + +
+ + + +
+
+ + + + + + + + + +
+ +
+ +
+ + +
+
+ + + +
+
+ + +
+ +
+
+ +
+ {{ leftTabTitle }} +
+
+ + + + +
+ + + +
+
+ + + +
+
+ + +
BASE KIT — MAPPED TO YOUR CODE
+ +
+ {{ item.glyph }} + {{ item.name }} + + + +
+
+
PATTERNS — YOURS
+ + +
+ + {{ item.name }} + +
+
+
+ +
Save a selection as a pattern — it lives here, in your system.
+
+
CUSTOM — YOUR CODE
+ +
+ +
+ {{ item.name }} + {{ item.file }} +
+ + +
+
+ + +
+ + +
COLOR — BRAND
+ + +
+
+
+ + + +
+
+ + +
+
+ edits ripple across canvas + code + +
+
+
+
+
+ + + +
+ {{ whereUsed }} +
COLOR — NEUTRAL
+
+ + + +
+
TYPE
+
+ Ag + +
+
NEUTRALS
+
+ + + +
+
RADIUS
+
+ radius-md + + + {{ radiusChip }} + +
+
SPACING
+
+ + + +
+ 4 · 8 · 12 · 16 · 24 · 32 · 48 +
+ +
HISTORY — EVERY CHANGE, LABELLED
+ +
No steps yet — every edit lands here with a label. Click one to travel back.
+
+ + + +
+ +
+
+
+ +
+
+ {{ canvasEl }} +
+ +
+
+ + + + +
+
+ ISOLATION + {{ isoName }} + {{ isoMap }} + + real runtime · variants × states + +
+
{{ isoEl }}
+
+
+ +
+ {{ bubbleTag }} + + + +
+
+
+
+ + + +
+ + + + + scroll — pan · ⌘ scroll — zoom · drag board — pan + +
+ +
+
+ + + + + + + + +
+
+ +
+
+
+ +
+ + COMPOSE — CONSTRAINED TO YOUR SYSTEM + + + +
+
+ +
+ +
+ + Describe a section — Veect composes it from your mapped components and tokens only. +
+ + + +
+ ◎ pick — scope an edit to one element
✎ draw — scribble a note on the board
+
+
+ +
+
+ {{ item.roleLabel }} + +
+ + ▧ {{ att.name }} + +
+
+ {{ item.text }} + +
+ + + staged as a ghost — nothing applied +
+
+ +
+ + +
+
+
+
+
+ +
+ + composing from your system… +
+
+
+
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+ + + + + + +
+ + +
+ + + + ▧ {{ att.name }} + + + ⏎ send · esc dismiss + +
+
+
+
+
+
+ + +
+ +
+
+ +
+ {{ rightTabTitle }} +
+
+ + + +
+ Select a component on the canvas. +
click — select · drag — move
⌘K — commands · ✦ — compose
⌘Z — undo · ⌫ — delete
+
+
+ +
+
+ {{ selGlyph }} +
+ {{ selName }} + {{ selType }} +
+
+ + +
+ FRAME + +
+ width +
+ + {{ frameWVal }} + +
+
+
+ active view + {{ frameViewInfo }} +
+
+
+ +
+ CONTENT + +
+
+ +
+ VARIANT +
+ + + +
+
+
+ +
+ SIZE +
+ + + +
+ WEIGHT +
+ + + +
+
+
+ +
+ TONE +
+ + + +
+ {{ toneNote }} +
+
+ +
+ LAYOUT +
+ + + +
+
+
+ gap +
+ + {{ selGap }} + +
+
+
+ padding +
+ + {{ selPad }} + +
+
+
+
+
+
+ + + +
+ {{ item.cat }} + {{ item.selectEl }} +
+
+ +
+
+
+ TOKENS BOUND + +
+ {{ item.prop }} + + {{ item.tok }} +
+
+
+
+ + + + + +
+
+
+
+ + +
+ +
+ + Critique this screen against your craft standard. + Spacing, hierarchy, contrast, alignment, states — every fix lands in your tokens and components, with the why. + + standard: Impeccable +
+
+ +
+
+ Scanning spacing, hierarchy, contrast… +
+
+ +
+ {{ polishCount }} + + + +
+ +
+
+ {{ item.sev }} + {{ item.title }} +
+ {{ item.why }} + +
+
+
+ +
+ + Impeccable. Nothing to fix. + Every check passed against your standard. + +
+
+
+
+ +
+
+
+ + +
+
+
+ + + + + + + + + + +
+ +
+
{{ codeDraftEl }}
+ +
+
+ +
{{ codeEl }}
+
+
+ prettier ✓tsc --noEmit ✓imports @solstice/ui +
+
+
+ +
+
+
+ + +
+
+
+ veect +
+

Sign in.

+

Your system is waiting — everything you compose exports as real React.

+ + +
+ + or + +
+ + + design-partner MVP — accounts are provisioned manually +
+
+
+ + +
+
+ veect + + + + +
+ + +
+
+ WORKSPACE · ONE REPO EACH + + + +
+ +
+
+
+ +
+
+
+
+ {{ connIcon }} +
+
+ Solstice + {{ connAccess }} +
+ workspace · one repo · solstice/web · {{ connMeta }} +
+ +
+
+

Projects

+ {{ homeMeta }} +
+
+ + + + +
+

Design systems

+
+
+
+ + + +
+
+
+ Solstice + {{ homeSysMeta }} +
+ + +
+
+ +
+
+
+
+
+ + +
+
+ + Settings + + +
+
+
+
+ CONNECTION +
+ {{ connIcon }} +
+
+ solstice/web + {{ connAccess }} +
+ {{ connMeta }} +
+ + +
+
+ + Tokens & components discovered from this repo — no paste, no upload. + {{ setSysMeta }} +
+
+
+ ENVIRONMENT +
+ +
+ {{ item.key }} + {{ item.masked }} + {{ item.src }} + {{ item.chip }} +
+
+
+ stays on this machine, encrypted · Veect never transmits your keys +
+
+ DOCTOR — CONNECTION HEALTH +
+ +
+ {{ item.dot }} + {{ item.name }} + {{ item.detail }} +
+
+
+
+
+ COMPONENT MAPPINGS — THE MERGE BAR +
+ +
+ {{ item.glyph }} + {{ item.name }} + {{ item.path }} + +
+
+
+ unmapped components fall back to the Veect base kit at export — the warning follows them +
+
+ CRAFT STANDARD — POLISH RULESET +
+ + + +
+
+
+ EXPORT DEFAULTS +
+ +
+ {{ item.label }} + {{ item.note }} + {{ item.chip }} +
+
+
+
+
+ PLAN — DESIGN PARTNER +
+
+ $0 during the MVP + billing starts at GA · seats 2 of 5 used · no card on file +
+ +
+
+ +
+
+
+
+ + +
+
+ + Profile + +
+
+
+
+ M +
+ Maya Kade + maya@solstice.team · Product designer +
+ DESIGN PARTNER +
+
+ PREFERENCES +
+ App theme +
+ + + +
+
+
+
+ WORKSPACE — SOLSTICE +
+ +
+ {{ item.initials }} +
+ {{ item.name }} + {{ item.role }} +
+ {{ item.chip }} +
+
+ +
+
+
+ KEYBOARD +
+ ⌘K — commands · ✦ — compose · ⌘Z — undo + ⌘0 — zoom to fit · ⌘1 — 100% · scroll — pan · ⌘scroll — zoom + ⌫ — delete selection · esc — dismiss anything +
+
+ +
+
+
+
+ + +
+
+ + System update + re-import tokens · diff · apply with undo + +
+
+
+ +

Devon updated the tokens. Paste the new file — Veect diffs it against the live system and shows the impact before anything changes.

+ + +
+ +
+ +
+ {{ item.tag }} + {{ item.tok }} + + {{ item.from }} + + + {{ item.to }} +
+
+
+
+ i + {{ sysImpact }} +
+
+ + +
+
+ +
+ + System updated. + radius-md 12 → 10 rippled across every bound component on the canvas and in the code. Bindings preserved — nothing broke silently. +
+ + +
+
+
+
+
+
+
+ + +
+
{{ previewEl }}
+
+ + preview — real components, real states + + +
+
+
+ + +
+
+
+
+ {{ exportTitle }} + PRODUCTION-GRADE + + +
+ Real components, real tokens — nothing inlined, nothing to translate. +
+ +
+ ! + Badge isn't mapped to your library — export falls back to the Veect default for it. + +
+
+
+
+ +
+ {{ item.name }} + + {{ item.note }} +
+
+
+
+ Ready to ship. +
npm i && npm run dev
+
+ · {{ exportImportsLine }} + · tokens as CSS variables — tokens.css + · deterministic — same tree, same code +
+ +
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + + +
+
⏎ run · ↑↓ navigate · esc close · ✦ routes to compose
+
+
+
+ +
+ +
+ {{ item.glyph }} + {{ item.text }} + + + + +
+
+
+ +
+
+ + + diff --git a/docs/product/reference/project/Weave - warm dark (backup).dc.html b/docs/product/reference/project/Weave - warm dark (backup).dc.html new file mode 100644 index 0000000..daeeb67 --- /dev/null +++ b/docs/product/reference/project/Weave - warm dark (backup).dc.html @@ -0,0 +1,3730 @@ + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+ + Weave + design-system-native canvas +
+

Bring your design system to life.

+

Weave turns your own tokens and components into real, production-ready React — on-brand, high-craft, nothing lost in translation.

+
+ + +
+
+ +
+
+ + + +

Paste your tokens

+

Tailwind config or CSS variables. Weave parses, contrast-checks, and re-themes the base kit — no hard stops.

+
+ +
+
+ PARSE + +
+ {{ item.glyph }} + {{ item.label }} + + {{ item.note }} +
+
+
+ +
+ CONTRAST — TOKEN PAIRS + +
+ {{ item.pair }} + + {{ item.ratio }} + {{ item.badge }} +
+
+
+
+ +
+
+
+ + + +

Start with five tokens

+

The essentials, set over the base kit. Everything stays a token — swap in the real file any time.

+
+
+
+ BRAND COLOR +
+ + + +
+
+
+ NEUTRALS +
+ + + +
+
+
+ CORNER RADIUS +
+ + {{ radiusLabel }} +
+
+
+ TYPE +
+ + + +
+
+
+ SPACING SCALE +
+ + + +
+
+ +
+
+
NEW
+
+ Email +
you@company.com
+
+
+
Get started
+
Docs
+
+ live preview — your tokens over the base kit +
+
+
+ + +
+ +

Here's the base kit — in Weave gray.

+
+ +

Your system is live.

+
+ {{ revealStat }} +
+
+
+
Get started
+
Secondary
+
+
+
NEW
+
Filter · 3
+
+
+ Email +
you@company.com
+
+
+
+
MK
+
+ Maya Kade + Product designer +
+
+
+
+
+
+ Overview + Members + Billing +
+
+
+
+
+
+
+ Notifications +
+
+
+
+
+
+ volume — 62 +
+
+ Ag + {{ revealFontNote }} +
+
+ +
+ + ⌘K anywhere · ✦ to compose +
+
+
+ +
+
+
+ + +
+ +
+ +
+ + +
+
+ +
+ Solstice — Marketing site + solstice system · autosaved +
+
+
+ + +
+
+ + + +
+
+
+
+
+ + + + {{ saveLabel }} + + +
+ + + +
+
+ + + + + + + + + + + +
+ +
+ +
+ + +
+
+ + + +
+
+ + +
+ +
+
+ +
+ {{ leftTabTitle }} +
+
+ + + + + + + + +
BASE KIT — MAPPED TO YOUR CODE
+ +
+ {{ item.glyph }} + {{ item.name }} + + + +
+
+
PATTERNS — YOURS
+ + +
+ + {{ item.name }} + +
+
+
+ +
Save a selection as a pattern — it lives here, in your system.
+
+
CUSTOM — YOUR CODE
+ +
+ +
+ {{ item.name }} + {{ item.file }} +
+ + +
+
+ + +
+ + +
COLOR — BRAND
+ + +
+
+
+ + + +
+
+ + +
+
+ edits ripple across canvas + code + +
+
+
+
+
+ + + +
+ {{ whereUsed }} +
COLOR — NEUTRAL
+
+ + + +
+
TYPE
+
+ Ag + +
+
NEUTRALS
+
+ + + +
+
RADIUS
+
+ radius-md + + + {{ radiusChip }} + +
+
SPACING
+
+ + + +
+ 4 · 8 · 12 · 16 · 24 · 32 · 48 +
+ +
HISTORY — EVERY CHANGE, LABELLED
+ +
No steps yet — every edit lands here with a label. Click one to travel back.
+
+ + + +
+ +
+
+
+ +
+
+ {{ canvasEl }} +
+ +
+
+ + + + +
+
+ ISOLATION + {{ isoName }} + {{ isoMap }} + + real runtime · variants × states + +
+
{{ isoEl }}
+
+
+ +
+ {{ bubbleTag }} + + + +
+
+
+
+ + + +
+ + + + + scroll — pan · ⌘ scroll — zoom · drag board — pan + +
+ +
+
+ + + + + +
+
+ +
+
+
+ +
+ + COMPOSE — CONSTRAINED TO YOUR SYSTEM + + + +
+
+ +
+ +
+ + Describe a section — Weave composes it from your mapped components and tokens only. +
+ + + +
+ ◎ pick — scope an edit to one element
✎ draw — scribble a note on the board
+
+
+ +
+
+ {{ item.roleLabel }} + +
+ + ▧ {{ att.name }} + +
+
+ {{ item.text }} + +
+ + + staged as a ghost — nothing applied +
+
+ +
+ + +
+
+
+
+
+ +
+ + composing from your system… +
+
+
+
+
+ +
+ + + +
+
+ +
+ + + +
+
+
+ + + + + + +
+ + +
+ + + + ▧ {{ att.name }} + + + ⏎ send · esc dismiss + +
+
+
+
+
+
+ + +
+ +
+
+ +
+ {{ rightTabTitle }} +
+
+ + + +
+ Select a component on the canvas. +
click — select · drag — move
⌘K — commands · ✦ — compose
⌘Z — undo · ⌫ — delete
+
+
+ +
+
+ {{ selGlyph }} +
+ {{ selName }} + {{ selType }} +
+
+ + +
+ FRAME + +
+ width +
+ + {{ frameWVal }} + +
+
+
+
+ +
+ CONTENT + +
+
+ +
+ VARIANT +
+ + + +
+
+
+ +
+ SIZE +
+ + + +
+ WEIGHT +
+ + + +
+
+
+ +
+ TONE +
+ + + +
+ {{ toneNote }} +
+
+ +
+ LAYOUT +
+ + + +
+
+
+ gap +
+ + {{ selGap }} + +
+
+
+ padding +
+ + {{ selPad }} + +
+
+
+
+
+
+ + + +
+ {{ item.cat }} + +
+
+ +
+
+
+ TOKENS BOUND + +
+ {{ item.prop }} + + {{ item.tok }} +
+
+
+
+ + + + + +
+
+
+
+ + +
+ +
+ + Critique this screen against your craft standard. + Spacing, hierarchy, contrast, alignment, states — every fix lands in your tokens and components, with the why. + + standard: Impeccable +
+
+ +
+
+ Scanning spacing, hierarchy, contrast… +
+
+ +
+ {{ polishCount }} + + + +
+ +
+
+ {{ item.sev }} + {{ item.title }} +
+ {{ item.why }} + +
+
+
+ +
+ + Impeccable. Nothing to fix. + Every check passed against your standard. + +
+
+
+
+ +
+
+
+ + +
+
+
+ + + + + + + + + + +
+ +
+
{{ codeDraftEl }}
+ +
+
+ +
{{ codeEl }}
+
+
+ prettier ✓tsc --noEmit ✓imports @solstice/ui +
+
+
+ +
+
+
+ + +
+
+
+ + Weave +
+

Sign in.

+

Your system is waiting — everything you compose exports as real React.

+ + +
+ + or + +
+ + + design-partner MVP — accounts are provisioned manually +
+
+
+ + +
+
+ + Weave + + + + + +
+
+
+
+

Projects

+ {{ homeMeta }} +
+
+ + + + +
+

Design systems

+
+
+
+ + + +
+
+
+ Solstice + {{ homeSysMeta }} +
+ + +
+
+ +
+
+
+
+
+ + +
+
+ + Settings + + +
+
+
+
+ DESIGN SYSTEM +
+ +
+ Solstice + {{ setSysMeta }} +
+ +
+ contrast — 3 pairs checked · brand body text passes AA at brand-700 +
+
+ COMPONENT MAPPINGS — THE MERGE BAR +
+ +
+ {{ item.glyph }} + {{ item.name }} + {{ item.path }} + +
+
+
+ unmapped components fall back to the Weave base kit at export — the warning follows them +
+
+ CRAFT STANDARD — POLISH RULESET +
+ + + +
+
+
+ EXPORT DEFAULTS +
+ +
+ {{ item.label }} + {{ item.note }} + {{ item.chip }} +
+
+
+
+
+ PLAN — DESIGN PARTNER +
+
+ $0 during the MVP + billing starts at GA · seats 2 of 5 used · no card on file +
+ +
+
+ +
+
+
+
+ + +
+
+ + Profile + +
+
+
+
+ M +
+ Maya Kade + maya@solstice.team · Product designer +
+ DESIGN PARTNER +
+
+ PREFERENCES +
+ App theme +
+ + + +
+
+
+
+ WORKSPACE — SOLSTICE +
+ +
+ {{ item.initials }} +
+ {{ item.name }} + {{ item.role }} +
+ {{ item.chip }} +
+
+ +
+
+
+ KEYBOARD +
+ ⌘K — commands · ✦ — compose · ⌘Z — undo + ⌘0 — zoom to fit · ⌘1 — 100% · scroll — pan · ⌘scroll — zoom + ⌫ — delete selection · esc — dismiss anything +
+
+ +
+
+
+
+ + +
+
+ + System update + re-import tokens · diff · apply with undo + +
+
+
+ +

Devon updated the tokens. Paste the new file — Weave diffs it against the live system and shows the impact before anything changes.

+ + +
+ +
+ +
+ {{ item.tag }} + {{ item.tok }} + + {{ item.from }} + + + {{ item.to }} +
+
+
+
+ i + {{ sysImpact }} +
+
+ + +
+
+ +
+ + System updated. + radius-md 12 → 10 rippled across every bound component on the canvas and in the code. Bindings preserved — nothing broke silently. +
+ + +
+
+
+
+
+
+
+ + +
+
{{ previewEl }}
+
+ + preview — real components, real states + + +
+
+
+ + +
+
+
+
+ {{ exportTitle }} + PRODUCTION-GRADE + + +
+ Real components, real tokens — nothing inlined, nothing to translate. +
+ +
+ ! + Badge isn't mapped to your library — export falls back to the Weave default for it. + +
+
+
+
+ +
+ {{ item.name }} + + {{ item.note }} +
+
+
+
+ Ready to ship. +
npm i && npm run dev
+
+ · {{ exportImportsLine }} + · tokens as CSS variables — tokens.css + · deterministic — same tree, same code +
+ +
+ + +
+
+
+
+
+
+ + +
+
+
+ + +
+
+ + + +
+
⏎ run · ↑↓ navigate · esc close · ✦ routes to compose
+
+
+
+ +
+ +
+ {{ item.glyph }} + {{ item.text }} + + + + +
+
+
+ +
+
+ + + diff --git a/docs/product/reference/project/support.js b/docs/product/reference/project/support.js new file mode 100644 index 0000000..2a774eb --- /dev/null +++ b/docs/product/reference/project/support.js @@ -0,0 +1,1702 @@ +// GENERATED from dc-runtime/src/*.ts — do not edit. Rebuild with `cd dc-runtime && bun run build`. +"use strict"; +(() => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + + // src/react.ts + function getReact() { + const R = window.React; + if (!R) throw new Error("dc-runtime: window.React is not available yet"); + return R; + } + function getReactDOM() { + const RD = window.ReactDOM; + if (!RD) throw new Error("dc-runtime: window.ReactDOM is not available yet"); + return RD; + } + var h = ((...args) => getReact().createElement( + ...args + )); + + // src/parse.ts + function parseDcDocument(doc) { + const dc = doc.querySelector("x-dc"); + if (!dc) return null; + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template: dc.innerHTML, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDcText(src) { + const openMatch = /]*)?>/.exec(src); + if (!openMatch) return null; + const close = src.lastIndexOf(""); + if (close === -1 || close < openMatch.index) return null; + const template = src.slice(openMatch.index + openMatch[0].length, close); + const doc = new DOMParser().parseFromString(src, "text/html"); + const scriptEl = doc.querySelector("script[data-dc-script]"); + const { props, preview } = parseDataProps( + scriptEl?.getAttribute("data-props") ?? null + ); + return { + template, + js: scriptEl ? scriptEl.textContent || "" : "", + props, + preview + }; + } + function parseDataProps(raw) { + if (!raw) return { props: null, preview: null }; + let parsed; + try { + parsed = JSON.parse(raw); + } catch { + return { props: null, preview: null }; + } + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + return { props: null, preview: null }; + } + const obj = parsed; + const preview = obj.$preview && typeof obj.$preview === "object" ? obj.$preview : null; + const rest = {}; + for (const k of Object.keys(obj)) { + if (k[0] !== "$") rest[k] = obj[k]; + } + return { props: Object.keys(rest).length ? rest : null, preview }; + } + function dcNameFromPath(pathname) { + let p = pathname || ""; + try { + p = decodeURIComponent(p); + } catch { + } + const base = p.split("/").pop() || "Root"; + return base.replace(/\.dc\.html$/, "").replace(/\.html?$/, "") || "Root"; + } + + // src/boot.ts + var BASE_CSS = ` + .sc-placeholder{background:color-mix(in srgb,currentColor 8%,transparent); + border:1px solid color-mix(in srgb,currentColor 50%,transparent); + border-radius:2px;box-sizing:border-box;overflow:hidden} + @keyframes sc-shine{0%{background-position:100% 50%}100%{background-position:0% 50%}} + html.sc-dc-streaming .sc-placeholder, + html.sc-dc-streaming .sc-interp.sc-missing{position:relative; + background:color-mix(in srgb,currentColor 5%,transparent); + border-color:transparent} + html.sc-dc-streaming .sc-placeholder::before, + html.sc-dc-streaming .sc-interp.sc-missing::before{content:''; + position:absolute;inset:0;pointer-events:none; + background:linear-gradient(90deg,rgba(217,119,87,0) 25%,rgba(247,225,211,.95) 37%,rgba(217,119,87,0) 63%); + background-size:400% 100%;animation:sc-shine 1.4s ease infinite} + html.sc-dc-streaming .sc-placeholder:nth-child(n+9 of .sc-placeholder)::before, + html.sc-dc-streaming .sc-interp.sc-missing:nth-child(n+9 of .sc-interp.sc-missing)::before{animation:none; + background:color-mix(in srgb,currentColor 8%,transparent)} + .sc-placeholder-error{padding:4px 8px;font:11px/1.4 ui-monospace,monospace; + color:color-mix(in srgb,currentColor 70%,transparent);word-break:break-word} + .sc-interp.sc-missing{display:inline-block;width:2em;height:1em;overflow:hidden; + vertical-align:text-bottom;background:rgba(255,255,255,.3);border:1px solid rgba(0,0,0,.5); + border-radius:2px;box-sizing:border-box;color:transparent; + user-select:none} + .sc-interp.sc-unresolved{font-family:ui-monospace,monospace;font-size:.85em; + color:color-mix(in srgb,currentColor 50%,transparent); + background:color-mix(in srgb,currentColor 10%,transparent);border-radius:3px; + padding:0 3px} + .sc-host.sc-has-error{position:relative} + .sc-logic-error{position:absolute;top:8px;left:8px;z-index:2147483647;max-width:60ch; + padding:6px 10px;background:#b00020;color:#fff;font:12px/1.4 ui-monospace,monospace; + border-radius:4px;white-space:pre-wrap;pointer-events:none} + /* Mirrors PRINT_BASELINE_CSS in apps/web deck-stage-export.ts \u2014 keep both + in sync until dc-runtime regains a build step. */ + @media print { + @page { margin: 0.5cm; } + figure, table { break-inside: avoid; } + #dc-root, #dc-root > .sc-host { height: auto; } + *, *::before, *::after { + print-color-adjust: exact; -webkit-print-color-adjust: exact; + backdrop-filter: none !important; -webkit-backdrop-filter: none !important; + animation-delay: -99s !important; animation-duration: .001s !important; + animation-iteration-count: 1 !important; animation-fill-mode: both !important; + animation-play-state: running !important; transition-duration: 0s !important; + } + } + `; + var FULL_PAGE_CSS = "html,body{height:100%;margin:0}#dc-root,#dc-root>.sc-host{height:100%}"; + function rootNameForDocument(doc, loc) { + let bootPath = loc.pathname || ""; + if (!/\.dc\.html?$/i.test(safeDecode(bootPath))) { + try { + bootPath = new URL(doc.baseURI || "/").pathname; + } catch { + } + } + return dcNameFromPath(bootPath); + } + function safeDecode(s) { + try { + return decodeURIComponent(s); + } catch { + return s; + } + } + function boot(runtime, doc = document) { + const parsed = parseDcDocument(doc); + if (!parsed) return null; + const React = getReact(); + const rootName = rootNameForDocument(doc, location); + runtime.markFetched(rootName); + runtime.setRootName(rootName); + runtime.adoptParsed(rootName, parsed); + if (!window.__resources) { + fetch(location.href).then((res) => res.ok ? res.text() : "").then((t) => { + const raw = t ? parseDcText(t) : null; + if (raw?.template) runtime.updateHtml(rootName, raw.template); + }).catch(() => { + }); + } + const dc = doc.querySelector("x-dc"); + const hostEl = doc.createElement("div"); + hostEl.id = "dc-root"; + dc.replaceWith(hostEl); + if (!parsed.preview) { + const s = doc.createElement("style"); + s.textContent = FULL_PAGE_CSS; + doc.head.appendChild(s); + } + const Root = runtime.getDC(rootName); + const entry = runtime.registry.get(rootName); + function StandaloneRoot() { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + entry.subs.add(sub); + return () => { + entry.subs.delete(sub); + }; + }, []); + const defaults = React.useMemo(() => { + const d = {}; + for (const k in entry.propsMeta || {}) { + const v = entry.propsMeta?.[k]?.default; + if (v !== void 0) d[k] = v; + } + return d; + }, [entry.propsMeta]); + return h(Root, { ...defaults, ...entry.propOverrides || {} }); + } + const ReactDOM = getReactDOM(); + if (ReactDOM.createRoot) + ReactDOM.createRoot(hostEl).render(h(StandaloneRoot)); + else ReactDOM.render(h(StandaloneRoot), hostEl); + return rootName; + } + + // src/expr.ts + var IDENT_RE = /^[A-Za-z_$][A-Za-z0-9_$]*/; + var NUMBER_RE = /^-?\d+(\.\d+)?$/; + function resolve(vals, src) { + const expr = String(src).trim(); + if (!expr) return void 0; + if (expr[0] === "(" && expr[expr.length - 1] === ")" && parensWrapWhole(expr)) { + return resolve(vals, expr.slice(1, -1)); + } + const eq = findTopLevelEquality(expr); + if (eq) { + const lv = resolve(vals, expr.slice(0, eq.index)); + const rv = resolve(vals, expr.slice(eq.index + eq.op.length)); + switch (eq.op) { + case "===": + return lv === rv; + case "!==": + return lv !== rv; + case "==": + return lv == rv; + default: + return lv != rv; + } + } + if (expr[0] === "!") return !resolve(vals, expr.slice(1)); + if (expr === "true") return true; + if (expr === "false") return false; + if (expr === "null") return null; + if (expr === "undefined") return void 0; + if (NUMBER_RE.test(expr)) return Number(expr); + if (expr.length >= 2 && (expr[0] === '"' || expr[0] === "'") && expr[expr.length - 1] === expr[0]) { + return expr.slice(1, -1); + } + return resolvePath(vals, expr); + } + function parensWrapWhole(expr) { + let depth = 0; + for (let i = 0; i < expr.length - 1; i++) { + if (expr[i] === "(") depth++; + else if (expr[i] === ")") { + depth--; + if (depth === 0) return false; + } + } + return true; + } + function findTopLevelEquality(expr) { + let depth = 0; + for (let i = 0; i < expr.length; i++) { + const c = expr[i]; + if (c === "[" || c === "(") depth++; + else if (c === "]" || c === ")") depth--; + else if (depth === 0 && (c === "=" || c === "!") && expr[i + 1] === "=") { + if (i > 0 && (expr[i - 1] === "=" || expr[i - 1] === "!")) continue; + if (!expr.slice(0, i).trim()) continue; + const op = expr[i + 2] === "=" ? c + "==" : c + "="; + return { index: i, op }; + } + } + return null; + } + function resolvePath(vals, expr) { + const head = expr.match(IDENT_RE); + if (!head) return void 0; + let cur = vals == null ? void 0 : vals[head[0]]; + let i = head[0].length; + while (i < expr.length) { + if (expr[i] === ".") { + const m = expr.slice(i + 1).match(IDENT_RE) || expr.slice(i + 1).match(/^\d+/); + if (!m) return void 0; + cur = cur == null ? void 0 : cur[m[0]]; + i += 1 + m[0].length; + } else if (expr[i] === "[") { + let depth = 1; + let j = i + 1; + while (j < expr.length && depth > 0) { + if (expr[j] === "[") depth++; + else if (expr[j] === "]") { + depth--; + if (depth === 0) break; + } + j++; + } + if (depth !== 0) return void 0; + const key = resolve(vals, expr.slice(i + 1, j)); + cur = cur == null ? void 0 : cur[key]; + i = j + 1; + } else { + return void 0; + } + } + return cur; + } + + // src/encode.ts + var CAMEL_ATTR = "sc-camel-"; + var INLINE_TEXT_TAGS = new Set( + "a abbr b bdi bdo br cite code del dfn em i ins kbd mark q s samp small span strike strong sub sup u var wbr".split( + " " + ) + ); + var RAW_WRAP = { + select: "sc-raw-select", + table: "sc-raw-table", + tbody: "sc-raw-tbody", + thead: "sc-raw-thead", + tfoot: "sc-raw-tfoot", + tr: "sc-raw-tr", + td: "sc-raw-td", + th: "sc-raw-th", + caption: "sc-raw-caption" + }; + var RAW_UNWRAP = Object.fromEntries( + Object.entries(RAW_WRAP).map(([k, v]) => [v, k]) + ); + var EVENT_MAP = { + onclick: "onClick", + onchange: "onChange", + oninput: "onInput", + onsubmit: "onSubmit", + onkeydown: "onKeyDown", + onkeyup: "onKeyUp", + onkeypress: "onKeyPress", + onmousedown: "onMouseDown", + onmouseup: "onMouseUp", + onmouseenter: "onMouseEnter", + onmouseleave: "onMouseLeave", + onfocus: "onFocus", + onblur: "onBlur", + ondoubleclick: "onDoubleClick", + oncontextmenu: "onContextMenu" + }; + var ATTRS = `(?:[^>"']|"[^"]*"|'[^']*')*`; + var IMPORT_SELF_CLOSE_RE = new RegExp( + "<(x-import|dc-import)(" + ATTRS + ")/>", + "gi" + ); + var CAMEL_ATTR_RE = /(\s)([a-z]+[A-Z][A-Za-z0-9]*)(\s*=)/g; + function encodeCase(html) { + html = html.replace( + IMPORT_SELF_CLOSE_RE, + (_, t, a) => "<" + t + a + ">" + ); + html = html.replace(/)/gi, "/gi, ""); + html = html.replace( + CAMEL_ATTR_RE, + (_, sp, name, eq) => sp + CAMEL_ATTR + name.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()) + eq + ); + for (const [real, alias] of Object.entries(RAW_WRAP)) { + html = html.replace( + new RegExp("(])", "gi"), + "$1" + alias + ); + } + return html; + } + function kebabToCamel(s) { + return s.replace(/-([a-z])/g, (_, c) => c.toUpperCase()); + } + function cssToObj(css) { + const o = {}; + for (const decl of css.split(";")) { + const i = decl.indexOf(":"); + if (i < 0) continue; + const prop = decl.slice(0, i).trim(); + o[prop.startsWith("--") ? prop : kebabToCamel(prop)] = decl.slice(i + 1).trim(); + } + return o; + } + function compileAttr(raw) { + const whole = raw.match(/^\s*\{\{([\s\S]+?)\}\}\s*$/); + if (whole) { + const path = whole[1]; + return (vals) => resolve(vals, path); + } + if (raw.includes("{{")) { + const parts = raw.split(/\{\{([\s\S]+?)\}\}/g); + return (vals) => parts.map((s, i) => i & 1 ? resolve(vals, s) ?? "" : s).join(""); + } + return () => raw; + } + + // src/compile.ts + function collectProps(node, kind, host) { + const propGetters = []; + const pseudoClasses = []; + let hintSize = null; + for (const { name, value } of [...node.attributes]) { + if (name === "sc-name" || name === "data-dc-tpl") continue; + let key = name; + if (key.startsWith(CAMEL_ATTR)) + key = kebabToCamel(key.slice(CAMEL_ATTR.length)); + if (key === "hint-size") { + hintSize = value; + continue; + } + if (key.startsWith("style-")) { + pseudoClasses.push(host.pseudoClass(key.slice(6), value)); + continue; + } + if (kind !== "dom") { + if (key.includes("-") && !(kind === "x-import" && (key.startsWith("aria-") || key.startsWith("data-")))) + key = kebabToCamel(key); + } else { + if (key === "class") key = "className"; + else if (key === "for") key = "htmlFor"; + else if (key.startsWith("on")) + key = EVENT_MAP[key] || "on" + key[2].toUpperCase() + key.slice(3); + } + propGetters.push([key, compileAttr(value)]); + } + return { propGetters, pseudoClasses, hintSize }; + } + var HOST_STYLE_PROPS = /* @__PURE__ */ new Set([ + "position", + "left", + "right", + "top", + "bottom", + "inset", + "width", + "height", + "z-index", + "transform" + ]); + function hostPositionStyle(style) { + const all = typeof style === "string" ? cssToObj(style) : style != null && typeof style === "object" ? style : null; + if (!all) return void 0; + const out = {}; + for (const [k, v] of Object.entries(all)) { + const kebab = k.replace(/[A-Z]/g, (c) => "-" + c.toLowerCase()); + if (HOST_STYLE_PROPS.has(kebab)) out[k] = v; + } + return Object.keys(out).length ? out : void 0; + } + function compileTemplate(html, host) { + const tpl = document.createElement("template"); + //! nosemgrep: direct-inner-html-assignment + tpl.innerHTML = encodeCase(html); + let tplN = 0; + (function stamp(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + node.setAttribute("data-dc-tpl", String(tplN++)); + } + for (const c of node.childNodes) stamp(c); + })(tpl.content); + const builders = walkChildren(tpl.content, host); + const render = ((vals, ctx) => builders.map((b, i) => b(vals || {}, ctx, i))); + render.__annotated = tpl.innerHTML; + return render; + } + function walkChildren(node, host) { + return [...node.childNodes].map((c) => walk(c, host)).filter((b) => b != null); + } + function walk(node, host) { + if (node.nodeType === Node.TEXT_NODE) return walkText(node); + if (node.nodeType !== Node.ELEMENT_NODE) return null; + const el = node; + const tag = el.tagName.toLowerCase(); + if (tag === "sc-for") return walkFor(el, host); + if (tag === "sc-if") return walkIf(el, host); + if (tag === "x-import") return walkXImport(el, host); + if (tag === "sc-helmet") return host.helmet(el); + if (tag === "dc-import") return walkComponent(el, host); + return walkElement(el, host); + } + var warnedHoles = /* @__PURE__ */ new Set(); + function warnUnresolved(ctx, what) { + const key = (ctx?.__name || "?") + "\0" + what; + if (warnedHoles.has(key)) return; + warnedHoles.add(key); + console.warn("[dc-runtime] " + (ctx?.__name || "template") + ": " + what); + } + function walkText(node) { + const txt = node.nodeValue ?? ""; + if (!txt.includes("{{")) { + if (!txt.trim() && !txt.includes(" ")) return null; + return () => txt; + } + const parts = txt.split(/\{\{([\s\S]+?)\}\}/g); + return (vals, ctx, key) => h( + getReact().Fragment, + { key }, + ...parts.map((p, i) => { + if (!(i & 1)) return p; + const v = resolve(vals, p); + if (v === void 0) { + if (!ctx?.__streamingNow) { + if (document.body?.hasAttribute("data-dc-editor-on")) { + return h( + "span", + { key: i, className: "sc-interp sc-unresolved" }, + "{{ " + p.trim() + " }}" + ); + } + warnUnresolved( + ctx, + "{{ " + p.trim() + " }} never resolved \u2014 rendered as empty" + ); + return null; + } + return h( + "span", + { key: i, className: "sc-interp sc-missing" }, + p.trim() + ); + } + if (getReact().isValidElement(v) || Array.isArray(v)) { + return h(getReact().Fragment, { key: i }, v); + } + if (v === null || typeof v === "boolean") return null; + return h("span", { key: i, className: "sc-interp" }, String(v)); + }) + ); + } + function walkFor(el, host) { + const listGet = compileAttr(el.getAttribute("list") || ""); + const asName = el.getAttribute("as") || "item"; + const hintN = parseInt(el.getAttribute("hint-placeholder-count") || "0", 10); + const kids = walkChildren(el, host); + const listSrc = el.getAttribute("list") || ""; + return (vals, ctx, key) => { + let list = listGet(vals); + if (!Array.isArray(list)) { + if (!ctx?.__streamingNow) { + if (list !== void 0 && list !== null) { + warnUnresolved( + ctx, + 'sc-for list="' + listSrc + '" is not an array (' + typeof list + ")" + ); + } + list = []; + } else { + list = hintN > 0 ? Array(hintN).fill(void 0) : []; + } + } + return h( + getReact().Fragment, + { key }, + list.map((item, i) => { + const sub = { ...vals, [asName]: item, $index: i }; + return h( + getReact().Fragment, + { key: i }, + kids.map((b, j) => b(sub, ctx, j)) + ); + }) + ); + }; + } + function walkIf(el, host) { + const valGet = compileAttr(el.getAttribute("value") || ""); + const hintRaw = el.getAttribute("hint-placeholder-val"); + const hintGet = hintRaw != null ? compileAttr(hintRaw) : null; + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + let v = valGet(vals); + if (v === void 0 && hintGet && ctx?.__streamingNow) v = hintGet(vals); + return v ? h( + getReact().Fragment, + { key }, + kids.map((b, j) => b(vals, ctx, j)) + ) : null; + }; + } + function walkComponent(el, host) { + const name = el.getAttribute("name") || el.getAttribute("component") || ""; + el.removeAttribute("name"); + el.removeAttribute("component"); + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const { propGetters, hintSize } = collectProps(el, "dc-import", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key, + __hintSize: hintSize, + __tplId: tplId, + __hostStyle: styleGet ? hostPositionStyle(styleGet(vals)) : void 0 + }; + for (const [k, g] of propGetters) { + const v = g(vals); + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return h(host.component(name), props); + }; + } + function walkXImport(el, host) { + const globalNameGet = compileAttr( + el.getAttribute("component-from-global-scope") || "" + ); + const exportNameGet = compileAttr( + el.getAttribute("component") || el.getAttribute("name") || "" + ); + const fromRaw = el.getAttribute("from") || (el.getAttribute("component-from-global-scope") ? "" : el.getAttribute("src") || el.getAttribute("import") || ""); + const urls = fromRaw.trim() ? fromRaw.trim().split(/\s+/) : []; + const url = urls.length ? urls[urls.length - 1] : ""; + const kindOf = (u) => /\.(jsx|tsx)(\?|#|$)/i.test(u) ? "jsx" : "js"; + const tplId = el.getAttribute("data-dc-tpl"); + const styleRaw = el.getAttribute("style"); + el.removeAttribute("style"); + const styleGet = styleRaw != null ? compileAttr(styleRaw) : null; + const wrap = tplId != null || styleGet != null; + const { propGetters, hintSize } = collectProps(el, "x-import", host); + const hasContent = el.children.length > 0 || !!(el.textContent || "").trim(); + const kids = hasContent ? walkChildren(el, host) : []; + const urlBindable = fromRaw.includes("{{"); + if (urls.length && !urlBindable) { + let prev; + for (const u of urls) prev = host.loadExternal(kindOf(u), u, prev); + } + const evalName = (g, vals) => { + const v = g(vals); + const s = v == null ? "" : String(v); + return s.includes("{{") ? "" : s; + }; + return (vals, ctx, key) => { + const globalName = evalName(globalNameGet, vals); + const name = globalName || evalName(exportNameGet, vals); + const C = !name || urlBindable ? null : globalName ? host.resolveExternalGlobal(url, globalName) : host.resolveExternal(url, name); + const hostStyle = styleGet ? hostPositionStyle(styleGet(vals)) : void 0; + const wrapper = wrap ? { + key, + className: "sc-host-x", + "data-dc-tpl": tplId, + style: hostStyle || { display: "contents" } + } : null; + if (!C) { + const error = urlBindable ? "x-import `from` cannot contain {{ \u2026 }} \u2014 module URLs are resolved at parse time; use a literal URL" : host.resolveExternalError(url, name); + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + const props = wrapper ? {} : { key }; + let unresolvedHole = false; + for (const [k, g] of propGetters) { + if (k === "component" || k === "componentFromGlobalScope" || k === "from") { + continue; + } + const v = g(vals); + if (v === void 0) unresolvedHole = true; + if (k === "dcProps") { + if (v && typeof v === "object") Object.assign(props, v); + continue; + } + props[k] = v; + } + if (unresolvedHole && ctx?.__htmlStreamingNow) { + const ph = host.placeholder({ + key: wrapper ? void 0 : key, + name, + hintSize, + error: null + }); + return wrapper ? h("div", wrapper, ph) : ph; + } + if (kids.length) props.children = kids.map((b, j) => b(vals, ctx, j)); + return wrapper ? h("div", wrapper, h(C, props)) : h(C, props); + }; + } + function contentKey(el) { + const clone = el.cloneNode(true); + for (const d of clone.querySelectorAll("*")) { + while (d.attributes.length) d.removeAttribute(d.attributes[0].name); + } + const s = clone.innerHTML; + let h2 = 5381; + for (let i = 0; i < s.length; i++) h2 = (h2 << 5) + h2 + s.charCodeAt(i) | 0; + return s.length + "." + (h2 >>> 0).toString(36); + } + var NEVER_CONTENT_KEYED = new Set( + "script style textarea option title select canvas iframe video audio".split( + " " + ) + ); + var NOT_INLINE_SELECTOR = ":not(" + [...INLINE_TEXT_TAGS].join(",") + ")"; + function walkElement(el, host) { + const realTag = RAW_UNWRAP[el.localName] || el.localName; + const tplId = el.getAttribute("data-dc-tpl"); + const inlineOnly = el.childNodes.length > 0 && !NEVER_CONTENT_KEYED.has(realTag) && el.querySelector(NOT_INLINE_SELECTOR) === null; + const keySuffix = inlineOnly ? "|" + contentKey(el) : ""; + const { propGetters, pseudoClasses } = collectProps(el, "dom", host); + const kids = walkChildren(el, host); + return (vals, ctx, key) => { + const props = { + key: key + keySuffix, + "data-dc-tpl": tplId + }; + for (const [k, g] of propGetters) { + let v = g(vals); + if (k === "style" && typeof v === "string") v = cssToObj(v); + if ((k === "value" || k === "checked") && v === void 0) { + v = k === "checked" ? false : ""; + } + props[k] = v; + } + if (pseudoClasses.length) { + props.className = [props.className, ...pseudoClasses].filter(Boolean).join(" "); + } + return h(realTag, props, ...kids.map((b, j) => b(vals, ctx, j))); + }; + } + + // src/logic.ts + var StreamableLogic = class { + constructor(props) { + __publicField(this, "props"); + __publicField(this, "state", {}); + /** Back-pointer to the wrapper component, installed after construction. */ + __publicField(this, "__host"); + this.props = props || {}; + } + setState(update, cb) { + this.__host && this.__host.__setLogicState(update, cb); + } + forceUpdate() { + this.__host && this.__host.forceUpdate(); + } + componentDidMount() { + } + componentDidUpdate(_prevProps) { + } + componentWillUnmount() { + } + /** The flat object the template renders against (merged over props). */ + renderVals() { + return {}; + } + }; + function evalDcLogic(src) { + //! nosemgrep: eval-and-function-constructor + const fn = new Function( + "DCLogic", + "StreamableLogic", + "React", + src + '\n;return (typeof Component!=="undefined"&&Component)||undefined;' + ); + return fn(StreamableLogic, StreamableLogic, getReact()); + } + + // src/component.ts + function shallowEqual(a, b) { + if (!b) return false; + const ak = Object.keys(a).filter((k) => k !== "children"); + const bk = Object.keys(b).filter((k) => k !== "children"); + if (ak.length !== bk.length) return false; + for (const k of ak) if (a[k] !== b[k]) return false; + return true; + } + function Placeholder({ + name, + hintSize, + streaming, + error + }) { + const [w, hgt] = (hintSize || "100%,60px").split(","); + return h( + "div", + { + className: "sc-placeholder" + (streaming ? " sc-streaming" : ""), + style: { width: w.trim(), height: hgt && hgt.trim() }, + title: name + }, + error ? h( + "div", + { className: "sc-placeholder-error" }, + (name ? name + ": " : "") + error + ) : null + ); + } + function hintToMin(hint) { + if (!hint) return void 0; + const [w, hgt] = hint.split(","); + return { minWidth: w.trim(), minHeight: hgt && hgt.trim() }; + } + function createComponentFactory(registry, ensureFetched) { + const React = getReact(); + const AncestorContext = React.createContext([]); + class StreamableComponent extends React.Component { + constructor(props) { + super(props); + __publicField(this, "__name"); + __publicField(this, "__sub"); + __publicField(this, "__needsDidMount", false); + /** Snapshot of the registry's streaming flags taken at render time — + * builders read it off the RenderCtx (this) to pick placeholder vs + * render-nothing for unresolved values. */ + __publicField(this, "__streamingNow", false); + __publicField(this, "__htmlStreamingNow", false); + /** When a construct throws, remember the (class, registry.ver, props) + * triple so render-time reconcile doesn't re-attempt it on every parent + * re-render. A registry bump (new class, template, external module + * resolving via bumpAll) changes `ver` and breaks the memo so an + * env-dependent constructor can self-heal. */ + __publicField(this, "__failedLogic", null); + __publicField(this, "__failedUserProps", null); + __publicField(this, "__failedVer", -1); + /** Per-instance constructor error — kept here (not on the registry entry) + * so one instance's successful construct can't hide a sibling's failure, + * and a construct can never wipe an eval error `updateJs` recorded on + * `r.logicError`. */ + __publicField(this, "__ctorError", null); + __publicField(this, "logic"); + this.__name = props.__name; + this.state = { __v: 0, __err: null }; + this.__sub = () => { + if (this.state.__err) this.setState({ __err: null }); + this.forceUpdate(); + }; + this.__makeLogic(registry.get(this.__name).Logic, null); + ensureFetched(this.__name); + } + /** Error-boundary hook: a render crash anywhere in this DC's subtree + * (its own template, an x-import'd component, a child DC without its + * own deeper boundary) lands here instead of unmounting the page. */ + static getDerivedStateFromError(e) { + return { __err: e instanceof Error && e.message ? e.message : String(e) }; + } + componentDidCatch(e, info) { + console.error( + "[dc-runtime] render error in <" + this.__name + ">:", + e, + info?.componentStack || "" + ); + } + /** Instantiate the logic class (or the no-op base) and adopt `prevState` + * over its initial state — used both at mount and on hot-swap. */ + __makeLogic(Logic, prevState) { + const L = Logic || StreamableLogic; + try { + this.logic = new L(this.__userProps()); + this.__failedLogic = null; + this.__failedUserProps = null; + this.__ctorError = null; + } catch (e) { + console.error(e); + this.__failedLogic = Logic; + this.__failedUserProps = this.__userProps(); + this.__failedVer = registry.get(this.__name).ver; + this.__ctorError = this.__name + ": " + (e instanceof Error && e.message ? e.message : String(e)); + this.logic = new StreamableLogic( + this.__userProps() + ); + } + this.logic.__host = this; + if (prevState) + this.logic.state = { ...this.logic.state || {}, ...prevState }; + } + /** The props the author's logic + template see — internal __-prefixed + * wiring stripped. */ + __userProps() { + const { __name, __hintSize, __tplId, __hostStyle, ...rest } = this.props; + return rest; + } + __setLogicState(update, cb) { + const prev = this.logic.state; + const patch = typeof update === "function" ? update(prev) : update; + this.logic.state = { ...prev, ...patch }; + this.setState((s) => ({ __v: s.__v + 1 }), cb); + } + /** Swap the logic instance when the registry's Logic class changed + * (streaming completion, hot reload). State carries over; didMount + * re-fires after the swap commits so refs exist. */ + __reconcileLogic() { + const r = registry.get(this.__name); + const Next = r.Logic; + const Cur = this.logic.constructor; + if (Next === Cur || !Next && Cur === StreamableLogic || Next === this.__failedLogic && r.ver === this.__failedVer && shallowEqual(this.__userProps(), this.__failedUserProps)) { + return; + } + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + this.__makeLogic(Next, this.logic.state); + this.__needsDidMount = true; + } + componentDidMount() { + registry.get(this.__name).subs.add(this.__sub); + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } + componentDidUpdate(prevProps) { + this.logic.props = this.__userProps(); + if (this.__needsDidMount) { + if (this.state.__err || !registry.get(this.__name).tpl) return; + this.__needsDidMount = false; + try { + this.logic.componentDidMount(); + } catch (e) { + console.error(e); + } + } else { + try { + this.logic.componentDidUpdate(prevProps); + } catch (e) { + console.error(e); + } + } + } + componentWillUnmount() { + registry.get(this.__name).subs.delete(this.__sub); + if (!this.__needsDidMount) { + try { + this.logic.componentWillUnmount(); + } catch (e) { + console.error(e); + } + } + } + render() { + const r = registry.get(this.__name); + const cls = "sc-host" + (r.htmlStreaming ? " sc-streaming-html" : "") + (r.jsStreaming ? " sc-streaming-js" : ""); + const hintStyle = r.htmlStreaming ? hintToMin(this.props.__hintSize) : void 0; + const hostStyle = this.props.__hostStyle || hintStyle ? { ...hintStyle || {}, ...this.props.__hostStyle || {} } : void 0; + const hostBase = { + className: cls, + style: hostStyle, + "data-sc-name": this.__name, + "data-dc-tpl": this.props.__tplId + }; + const chain = Array.isArray(this.context) ? this.context : []; + if (chain.includes(this.__name)) { + const cycle = [ + ...chain.slice(chain.indexOf(this.__name)), + this.__name + ].join(" \u2192 "); + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: "circular import: " + cycle + }) + ); + } + if (this.state.__err) { + return h( + "div", + { ...hostBase, className: cls + " sc-has-error" }, + h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + this.__name + ": " + this.state.__err + ), + h(Placeholder, { + name: this.__name, + hintSize: this.props.__hintSize, + error: this.state.__err + }) + ); + } + this.__reconcileLogic(); + if (!r.tpl) { + return h( + "div", + hostBase, + h(Placeholder, { name: this.__name, hintSize: this.props.__hintSize }) + ); + } + const userProps = this.__userProps(); + this.logic.props = userProps; + let vals = userProps; + let renderErr = r.logicError || this.__ctorError; + try { + vals = { ...userProps, ...this.logic.renderVals() || {} }; + } catch (e) { + console.error(e); + renderErr = this.__name + ".renderVals(): " + (e instanceof Error && e.message ? e.message : String(e)); + } + this.__streamingNow = !!(r.htmlStreaming || r.jsStreaming); + this.__htmlStreamingNow = !!r.htmlStreaming; + return h( + "div", + { ...hostBase, className: cls + (renderErr ? " sc-has-error" : "") }, + renderErr && h( + "div", + { className: "sc-logic-error", "data-omelette-chrome": "" }, + renderErr + ), + h( + AncestorContext.Provider, + { value: [...chain, this.__name] }, + r.tpl(vals, this) + ) + ); + } + } + __publicField(StreamableComponent, "contextType", AncestorContext); + const named = /* @__PURE__ */ new Map(); + function getDC(name) { + const hit = named.get(name); + if (hit) return hit; + function Dispatcher(p) { + const [, setTick] = React.useState(0); + React.useEffect(() => { + const sub = () => setTick((n) => n + 1); + registry.get(name).subs.add(sub); + return () => { + registry.get(name).subs.delete(sub); + }; + }, []); + ensureFetched(name); + return h(StreamableComponent, { ...p, __name: name }); + } + Dispatcher.displayName = name; + named.set(name, Dispatcher); + return Dispatcher; + } + return { + getDC, + StreamableComponent + }; + } + + // src/cdn.ts + var REACT_URL = "https://unpkg.com/react@18.3.1/umd/react.production.min.js"; + var REACT_SRI = "sha384-DGyLxAyjq0f9SPpVevD6IgztCFlnMF6oW/XQGmfe+IsZ8TqEiDrcHkMLKI6fiB/Z"; + var REACT_DOM_URL = "https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js"; + var REACT_DOM_SRI = "sha384-gTGxhz21lVGYNMcdJOyq01Edg0jhn/c22nsx0kyqP0TxaV5WVdsSH1fSDUf5YJj1"; + var BABEL_URL = "https://unpkg.com/@babel/standalone@7.29.0/babel.min.js"; + var BABEL_SRI = "sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y"; + function cdnScriptFor(url, sri) { + const res = window.__resources; + const v = res ? res[url] : void 0; + return typeof v === "string" && v ? { src: v } : { src: url, integrity: sri }; + } + + // src/external.ts + var isCustomElementName = (n) => !n.includes(".") && n.includes("-"); + function isRenderableType(g) { + if (typeof g === "function") return !isElementClass(g); + return typeof g === "object" && g !== null && typeof g.$$typeof === "symbol"; + } + function resolveDottedPath(root, name) { + let cur = root; + for (const seg of name.split(".")) { + if (cur == null) return void 0; + cur = cur[seg]; + } + return cur; + } + var GLOBAL_POLL_INTERVAL_MS = 50; + var GLOBAL_POLL_TIMEOUT_MS = 3e4; + function createExternalModules(onResolved) { + const cache = /* @__PURE__ */ new Map(); + let babelLoading = null; + const reportedMissing = /* @__PURE__ */ new Map(); + const polling = /* @__PURE__ */ new Set(); + function ensureBabel() { + if (window.Babel) return Promise.resolve(); + if (babelLoading) return babelLoading; + const babel = cdnScriptFor(BABEL_URL, BABEL_SRI); + babelLoading = new Promise((res, rej) => { + const s = document.createElement("script"); + s.src = babel.src; + if (babel.integrity) { + s.integrity = babel.integrity; + s.crossOrigin = "anonymous"; + } + s.onload = () => res(); + s.onerror = rej; + document.head.appendChild(s); + }); + return babelLoading; + } + const pending = /* @__PURE__ */ new Map(); + function load(kind, url, after) { + const existing = pending.get(url); + if (existing) return existing; + cache.set(url, null); + console.info("[dc-runtime] x-import: loading", url, "(" + kind + ")"); + const ready = Promise.all([ + kind === "jsx" ? ensureBabel() : Promise.resolve(), + after ?? Promise.resolve() + ]); + const p = ready.then(() => fetch(url)).then((r) => { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.text(); + }).then((src) => { + const code = kind === "jsx" ? window.Babel.transform(src, { + filename: url, + presets: ["react", "typescript"] + }).code : src; + const module = { exports: {} }; + const before = new Set(Object.keys(window)); + //! nosemgrep: eval-and-function-constructor + new Function("React", "module", "exports", "require", code)( + getReact(), + module, + module.exports, + () => ({}) + ); + const globals = {}; + for (const k of Object.keys(window)) { + if (!before.has(k) && typeof window[k] === "function") { + globals[k] = window[k]; + } + } + cache.set(url, { mod: module.exports, globals }); + console.info( + "[dc-runtime] x-import: loaded", + url, + "\u2014 exports:", + Object.keys(module.exports), + "window globals:", + Object.keys(globals) + ); + onResolved(); + }).catch((e) => { + cache.set(url, { + mod: {}, + globals: {}, + error: "failed to load: " + (e instanceof Error && e.message ? e.message : String(e)) + }); + console.error( + "[dc-runtime] x-import: FAILED to load", + url, + "(" + kind + ")", + e + ); + onResolved(); + }); + pending.set(url, p); + return p; + } + function resolve2(url, name) { + const entry = cache.get(url); + if (!entry) return null; + const { mod, globals } = entry; + const C = mod && mod[name] || globals && globals[name] || typeof window !== "undefined" && window[name] || mod && mod.default; + if (typeof C === "function") return C; + const key = url + "\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set( + key, + entry.error || 'no export named "' + name + '" (has: ' + Object.keys(mod).join(", ") + ")" + ); + console.error( + "[dc-runtime] x-import: module", + url, + "loaded but has no component named", + JSON.stringify(name), + "\u2014 available exports:", + Object.keys(mod), + "window globals:", + Object.keys(globals), + ". The module must `module.exports = {" + name + "}` or set `window." + name + "`." + ); + } + return null; + } + function waitForGlobal(name) { + if (polling.has(name)) return; + polling.add(name); + const started = Date.now(); + const isCE = isCustomElementName(name); + const tick = () => { + const found = isCE ? customElements.get(name) : isRenderableType(resolveDottedPath(window, name)); + if (found) { + polling.delete(name); + onResolved(); + return; + } + if (Date.now() - started >= GLOBAL_POLL_TIMEOUT_MS) { + console.warn( + "[dc-runtime] x-import: global", + JSON.stringify(name), + "never appeared on window after " + GLOBAL_POLL_TIMEOUT_MS + "ms" + ); + return; + } + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + }; + setTimeout(tick, GLOBAL_POLL_INTERVAL_MS); + } + function resolveGlobal(url, name) { + const isCE = isCustomElementName(name); + if (!url) { + if (isCE) { + if (customElements.get(name)) return name; + waitForGlobal(name); + return null; + } + const g2 = resolveDottedPath(window, name); + if (isRenderableType(g2)) return g2; + waitForGlobal(name); + return null; + } + const entry = cache.get(url); + if (!entry) return null; + if (isCE && customElements.get(name)) return name; + const g = entry.globals[name] ?? resolveDottedPath(window, name); + if (isRenderableType(g)) return g; + if (name.includes(".")) return null; + const key = url + "\0global\0" + name; + if (!reportedMissing.has(key)) { + reportedMissing.set(key, null); + if (isCE && !customElements.get(name)) { + console.warn( + "[dc-runtime] x-import:", + url, + "loaded but no custom element", + JSON.stringify(name), + "is registered and window." + name + " is not a function \u2014 rendering <" + name + "> as an unknown element." + ); + } + } + return name; + } + function getError(url, name) { + const entry = cache.get(url); + if (entry?.error) return entry.error; + return reportedMissing.get(url + "\0" + name) || null; + } + return { load, resolve: resolve2, resolveGlobal, getError }; + } + function isElementClass(g) { + try { + return typeof g === "function" && typeof HTMLElement !== "undefined" && g.prototype instanceof HTMLElement; + } catch { + return false; + } + } + + // src/atomics.ts + var ATOMIC_CSS = ( + // layout + ".fx{display:flex}.col{display:flex;flex-direction:column}.grid{display:grid}.ac{align-items:center}.jc{justify-content:center}.jb{justify-content:space-between}.f1{flex:1}.noshrink{flex-shrink:0}.wrap{flex-wrap:wrap}.fw5{font-weight:500}.fw6{font-weight:600}.fw7{font-weight:700}.fw8{font-weight:800}.fs11{font-size:11px}.fs12{font-size:12px}.fs13{font-size:13px}.fs14{font-size:14px}.fs15{font-size:15px}.fs16{font-size:16px}.fs20{font-size:20px}.fs22{font-size:22px}.upper{text-transform:uppercase}.tc{text-align:center}.nowrap{white-space:nowrap}.gap8{gap:8px}.gap10{gap:10px}.gap12{gap:12px}.gap16{gap:16px}.gap24{gap:24px}.m0{margin:0}.mt8{margin-top:8px}.mt12{margin-top:12px}.mt16{margin-top:16px}.mb8{margin-bottom:8px}.mb12{margin-bottom:12px}.mb16{margin-bottom:16px}.posrel{position:relative}.posabs{position:absolute}.round{border-radius:50%}.ohide{overflow:hidden}.bbox{box-sizing:border-box}.pointer{cursor:pointer}.w100{width:100%}.b0{border:none}" + ); + + // src/helmet.ts + var DESIGN_DOC_MODE_RE = /]*\bname\s*=\s*["']design_doc_mode["'][^>]*\b(?:content|value)\s*=\s*["'](\w+)["']/i; + var CANVAS_BG_LIGHT = "#f0eee6"; + var CANVAS_BG_DARK = "#2e2c26"; + function createHelmetManager(doc, isStreaming) { + const mounted = /* @__PURE__ */ new Set(); + const live = /* @__PURE__ */ new Map(); + let designDocMode = null; + let canvasStyleEl = null; + let appTheme = "light"; + try { + const ds = doc.documentElement.dataset.theme; + appTheme = ds === "dark" || ds === "light" ? ds : new URLSearchParams(doc.defaultView?.location.search ?? "").get( + "theme" + ) === "dark" ? "dark" : "light"; + } catch { + } + function applyCanvasBg() { + if (!canvasStyleEl) return; + const bg = appTheme === "dark" ? CANVAS_BG_DARK : CANVAS_BG_LIGHT; + canvasStyleEl.textContent = `html,body{background:${bg}}#dc-root>.sc-host{position:relative}`; + } + function postDesignMode(mode) { + if (window.parent === window) return; + try { + window.parent.postMessage({ type: "__dc_design_mode", mode }, "*"); + } catch { + } + } + function setDesignDocMode(mode) { + if (mode === designDocMode) return; + designDocMode = mode; + postDesignMode(mode); + if (mode === "canvas") { + doc.documentElement.setAttribute("data-dc-canvas", ""); + canvasStyleEl = doc.createElement("style"); + canvasStyleEl.setAttribute("data-dc-canvas", ""); + applyCanvasBg(); + doc.head.appendChild(canvasStyleEl); + } else { + doc.documentElement.removeAttribute("data-dc-canvas"); + canvasStyleEl?.remove(); + canvasStyleEl = null; + } + } + window.addEventListener("message", (e) => { + const type = e.data && e.data.type; + if (type === "__dc_theme") { + const t = e.data.theme; + if (t === "light" || t === "dark") { + appTheme = t; + applyCanvasBg(); + } + return; + } + if (!designDocMode || type !== "__dc_probe") return; + postDesignMode(designDocMode); + }); + function compile(node) { + const raw = [...node.children]; + const helmetClosed = node.nextSibling != null || node.parentNode?.nextSibling != null; + if (node.hasAttribute("data-dc-atomics") && !mounted.has("__dc-atomics")) { + mounted.add("__dc-atomics"); + const el = doc.createElement("style"); + el.id = "__dc-atomics"; + el.textContent = ATOMIC_CSS; + doc.head.appendChild(el); + } + return (_vals, ctx) => { + const name = ctx && ctx.__name || ""; + const streaming = !!(name && isStreaming(name)); + for (let i = 0; i < raw.length; i++) { + const child = raw[i]; + const tag = child.tagName; + const mayBePartial = streaming && !helmetClosed && i === raw.length - 1; + if (tag === "SCRIPT") { + if (mayBePartial) continue; + const key = "SCRIPT|" + (child.getAttribute("src") || child.textContent || ""); + if (mounted.has(key)) continue; + mounted.add(key); + const el = doc.createElement("script"); + for (const { name: an, value } of [...child.attributes]) + el.setAttribute(an, value); + if (child.textContent) el.textContent = child.textContent; + doc.head.appendChild(el); + } else if (tag === "LINK" || tag === "META") { + if (mayBePartial) continue; + const key = tag + "|" + (child.getAttribute("href") || child.getAttribute("src") || child.outerHTML); + if (mounted.has(key)) continue; + mounted.add(key); + doc.head.appendChild(child.cloneNode(true)); + } else { + const key = name + "|" + i; + let el = live.get(key); + if (!el || el.tagName !== tag) { + if (el) el.remove(); + el = doc.createElement(tag.toLowerCase()); + live.set(key, el); + doc.head.appendChild(el); + } + for (const { name: an, value } of [...child.attributes]) { + if (el.getAttribute(an) !== value) el.setAttribute(an, value); + } + if (el.textContent !== child.textContent) + el.textContent = child.textContent; + } + } + return null; + }; + } + return { compile, setDesignDocMode }; + } + + // src/pseudo.ts + function createPseudoSheet(doc) { + let el = null; + const cache = /* @__PURE__ */ new Map(); + let n = 0; + return (pseudo, css) => { + const k = pseudo + "|" + css; + const hit = cache.get(k); + if (hit) return hit; + if (!el) { + el = doc.createElement("style"); + doc.head.appendChild(el); + } + const cls = "scp" + (n++).toString(36); + const sel = pseudo === "before" || pseudo === "after" ? "." + cls + "::" + pseudo : "." + cls + ":" + pseudo; + el.sheet.insertRule(sel + "{" + css + "}", el.sheet.cssRules.length); + cache.set(k, cls); + return cls; + }; + } + + // src/registry.ts + function createRegistry() { + const entries = /* @__PURE__ */ Object.create(null); + function get(name) { + return entries[name] || (entries[name] = { + html: "", + tpl: null, + Logic: null, + jsStreaming: false, + htmlStreaming: false, + ver: 0, + subs: /* @__PURE__ */ new Set(), + fetched: false + }); + } + function bump(name) { + const r = get(name); + r.ver++; + for (const fn of r.subs) fn(); + } + return { + entries, + get, + bump, + bumpAll() { + for (const n in entries) bump(n); + } + }; + } + + // src/runtime.ts + var COMPONENT_DIR = "."; + function createRuntime(doc = document) { + const registry = createRegistry(); + const pseudoClass = createPseudoSheet(doc); + const helmet = createHelmetManager( + doc, + (name) => registry.get(name).htmlStreaming + ); + const external = createExternalModules(() => registry.bumpAll()); + const factory = createComponentFactory(registry, ensureFetched); + const host = { + component: (name) => factory.getDC(name), + placeholder: (props) => h(Placeholder, props), + helmet: (node) => helmet.compile(node), + loadExternal: (kind, url, after) => external.load(kind, url, after), + resolveExternal: (url, name) => external.resolve(url, name), + resolveExternalGlobal: (url, name) => external.resolveGlobal(url, name), + resolveExternalError: (url, name) => external.getError(url, name), + pseudoClass + }; + function ensureFetched(name) { + const r = registry.get(name); + if (r.fetched) return; + r.fetched = true; + const url = COMPONENT_DIR + "/" + encodeURIComponent(name) + ".dc.html"; + fetch(url).then((res) => { + if (!res.ok) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '" failed:', + url, + "returned", + res.status, + "\u2014 the reference renders as an empty placeholder." + ); + return ""; + } + return res.text(); + }).then((t) => { + if (!t) return; + const parsed = parseDcText(t); + if (!parsed) { + console.error( + '[dc-runtime] sibling fetch for "' + name + '":', + url, + "has no block \u2014 not a Design Component." + ); + return; + } + if (parsed.props) r.propsMeta = parsed.props; + if (parsed.preview) r.preview = parsed.preview; + if (parsed.template && !r.html) updateHtml(name, parsed.template); + if (parsed.js && !r.Logic) updateJs(name, parsed.js); + }).catch( + (e) => console.error( + '[dc-runtime] sibling fetch for "' + name + '" threw:', + url, + e + ) + ); + } + let rootName = null; + function updateHtml(name, html) { + const r = registry.get(name); + r.html = html; + if (name === rootName) { + const mode = DESIGN_DOC_MODE_RE.exec(html)?.[1] ?? null; + if (mode || !r.htmlStreaming) helmet.setDesignDocMode(mode); + } + try { + r.tpl = compileTemplate(html, host); + } catch (e) { + console.error("[dc-runtime] template compile FAILED for", name, e); + } + registry.bump(name); + } + function updateJs(name, src) { + const r = registry.get(name); + const seq = r.jsSeq = (r.jsSeq || 0) + 1; + try { + const Cls = evalDcLogic(src); + if (r.jsSeq !== seq) return; + if (typeof Cls !== "function") { + r.logicError = name + ".dc.html: + + diff --git a/docs/product/reference/project/veect-codebase/package.json b/docs/product/reference/project/veect-codebase/package.json new file mode 100644 index 0000000..92e5d90 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/package.json @@ -0,0 +1,47 @@ +{ + "name": "veect", + "private": true, + "version": "0.3.0", + "description": "Veect — design-system-native canvas. Your tokens and components in, production React out.", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc --noEmit && vite build", + "preview": "vite preview", + "lint": "eslint src --ext .ts,.tsx", + "format": "prettier --write \"src/**/*.{ts,tsx,css}\"", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@radix-ui/react-dialog": "^1.1.1", + "@radix-ui/react-dropdown-menu": "^2.1.1", + "@radix-ui/react-select": "^2.1.1", + "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.0", + "@radix-ui/react-tooltip": "^1.1.2", + "@xyflow/react": "^12.3.2", + "class-variance-authority": "^0.7.0", + "clsx": "^2.1.1", + "lucide-react": "^0.428.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "tailwind-merge": "^2.5.2", + "zustand": "^4.5.4" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.18.0", + "@typescript-eslint/parser": "^7.18.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.20", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.9", + "postcss": "^8.4.41", + "prettier": "^3.3.3", + "tailwindcss": "^3.4.10", + "typescript": "^5.5.4", + "vite": "^5.4.2" + } +} diff --git a/docs/product/reference/project/veect-codebase/postcss.config.js b/docs/product/reference/project/veect-codebase/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/docs/product/reference/project/veect-codebase/src/App.tsx b/docs/product/reference/project/veect-codebase/src/App.tsx new file mode 100644 index 0000000..2a872c3 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/App.tsx @@ -0,0 +1,48 @@ +import { useEffect } from 'react'; +import { EditorPage } from '@/pages/EditorPage'; +import { HomePage } from '@/pages/HomePage'; +import { OnboardingPage } from '@/pages/OnboardingPage'; +import { ProfilePage } from '@/pages/ProfilePage'; +import { SettingsPage } from '@/pages/SettingsPage'; +import { SignInPage } from '@/pages/SignInPage'; +import { SystemUpdatePage } from '@/pages/SystemUpdatePage'; +import { useVeect } from '@/store/veect'; +import type { Screen } from '@/types'; + +const PAGES: Record JSX.Element> = { + signin: SignInPage, + home: HomePage, + onboarding: OnboardingPage, + editor: EditorPage, + settings: SettingsPage, + profile: ProfilePage, + sysupdate: SystemUpdatePage, +}; + +export default function App() { + const screen = useVeect((s) => s.screen); + const undo = useVeect((s) => s.undo); + const redo = useVeect((s) => s.redo); + const select = useVeect((s) => s.select); + + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + const typing = ['INPUT', 'TEXTAREA'].includes((e.target as HTMLElement).tagName); + if (e.key === 'Escape') return select(null); + if (typing) return; + if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'z') { + e.preventDefault(); + (e.shiftKey ? redo : undo)(); + } + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, [redo, select, undo]); + + const Page = PAGES[screen]; + return ( +
+ +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/atoms/Logo.tsx b/docs/product/reference/project/veect-codebase/src/components/atoms/Logo.tsx new file mode 100644 index 0000000..5835366 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/atoms/Logo.tsx @@ -0,0 +1,14 @@ +/** + * Atom — the Veect logo. Typographic: a tight semibold lowercase + * wordmark closed by a square ink terminal (the vector point). + * Inherits currentColor, so it holds in both themes and on the landing. + */ +export function Logo({ size = 14 }: { size?: number }) { + const sq = Math.max(3, Math.round(size * 0.28)); + return ( + + veect + + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/atoms/primitives.tsx b/docs/product/reference/project/veect-codebase/src/components/atoms/primitives.tsx new file mode 100644 index 0000000..fb9feab --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/atoms/primitives.tsx @@ -0,0 +1,28 @@ +import type { PropsWithChildren } from 'react'; +import { cn } from '@/lib/utils'; + +/** Atom — uppercase mono section label (the engraved-instrument voice). */ +export function MonoLabel({ children, className }: PropsWithChildren<{ className?: string }>) { + return ( + + {children} + + ); +} + +/** Atom — color swatch with an inset hairline so light colors hold shape. */ +export function Swatch({ hex, size = 16, radius = 2 }: { hex: string; size?: number; radius?: number }) { + return ( + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/ChatBubble.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/ChatBubble.tsx new file mode 100644 index 0000000..1a58a10 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/ChatBubble.tsx @@ -0,0 +1,36 @@ +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import type { ChatMessage } from '@/types'; + +/** Molecule — one chat turn. User turns sit right in accent-dim; assistant turns left in chip. */ +export function ChatBubble({ + message, + actions, +}: { + message: ChatMessage; + actions?: React.ReactNode; +}) { + const user = message.role === 'user'; + return ( +
+
+ {message.atts && message.atts.length > 0 && ( +
+ {message.atts.map((a) => ( + ▧ {a.name} + ))} +
+ )} + {message.text} + {actions} +
+
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/ChatComposer.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/ChatComposer.tsx new file mode 100644 index 0000000..1cd2f62 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/ChatComposer.tsx @@ -0,0 +1,77 @@ +import { useState } from 'react'; +import { MonoLabel } from '@/components/atoms/primitives'; +import { Badge } from '@/components/ui/badge'; +import { Input } from '@/components/ui/input'; +import { Select } from '@/components/ui/select'; +import { truncate } from '@/lib/utils'; +import type { VeectNode } from '@/types'; + +/** + * Molecule — the chat composer: scope chip, input pill, send, model picker. + * Store-free: state in, events out. + */ +export function ChatComposer({ + targetNode, + onClearTarget, + onSubmit, + thinking, + model, + onModel, +}: { + targetNode: VeectNode | null; + onClearTarget: () => void; + onSubmit: (text: string) => void; + thinking: boolean; + model: string; + onModel: (m: string) => void; +}) { + const [draft, setDraft] = useState(''); + + const send = () => { + const text = draft.trim(); + if (!text || thinking) return; + setDraft(''); + onSubmit(text); + }; + + return ( +
+ {targetNode && ( +
+ + {targetNode.type} · {truncate(targetNode.name ?? targetNode.props.text ?? targetNode.type, 16)} + + +
+ )} +
+ + setDraft(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && send()} + placeholder={targetNode ? `refine further — scoped to this ${targetNode.type}…` : 'Describe a section — composed only from your system…'} + className="border-none bg-transparent px-0" + /> + +
+
+ + + constrained to your 10 components +
+
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/CollapseButton.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/CollapseButton.tsx new file mode 100644 index 0000000..163d566 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/CollapseButton.tsx @@ -0,0 +1,30 @@ +import { cn } from '@/lib/utils'; + +/** + * Molecule — the collapse chevron shown inside an open panel. Panels + * reopen from the ActivityBar, so this is the only dismiss affordance + * a panel needs. + */ +export function CollapseButton({ + direction, + onCollapse, + className, +}: { + direction: 'left' | 'right'; + onCollapse: () => void; + className?: string; +}) { + return ( + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/LayerTree.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/LayerTree.tsx new file mode 100644 index 0000000..2bab4b9 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/LayerTree.tsx @@ -0,0 +1,121 @@ +import { useState } from 'react'; +import { NODE_GLYPHS } from '@/config/library'; +import { activeView, VIEW_ORDER, viewsOf, viewW, type ViewKind } from '@/engine/views'; +import { cn } from '@/lib/utils'; +import type { VeectNode } from '@/types'; + +const EyeIcon = ({ off }: { off?: boolean }) => ( + + + {off ? ( + + ) : ( + + )} + +); + +/** + * Molecule — collapsible layer tree. Under every frame row sits a + * clearly separated variant-chips row (desktop/tablet/mobile): the dot + * toggles a view's visibility, the label makes it the active view. + */ +export function LayerTree({ + frames, + selectedId, + onSelect, + onSetViews, +}: { + frames: VeectNode[]; + selectedId: string | null; + onSelect: (id: string) => void; + onSetViews: (frameId: string, views: ViewKind[], view: ViewKind) => void; +}) { + const [expanded, setExpanded] = useState>({}); + + const rows: Array<{ node: VeectNode; depth: number; branch: boolean }> = []; + const push = (node: VeectNode, depth: number) => { + const branch = node.children.length > 0; + rows.push({ node, depth, branch }); + if (expanded[node.id]) node.children.forEach((c) => push(c, depth + 1)); + }; + frames.forEach((f) => push(f, 0)); + + return ( +
+ {rows.map(({ node, depth, branch }) => ( +
+ + + {node.type === 'frame' && ( +
+ {VIEW_ORDER.map((k) => { + const views = viewsOf(node); + const av = activeView(node); + const on = views.includes(k); + const isAv = av === k && on; + return ( + + ); + })} +
+ )} +
+ ))} +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/LibraryList.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/LibraryList.tsx new file mode 100644 index 0000000..53914ae --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/LibraryList.tsx @@ -0,0 +1,73 @@ +import { LIBRARY, NODE_GLYPHS } from '@/config/library'; +import { CUSTOMER_LIB } from '@/config/demo'; +import { MonoLabel } from '@/components/atoms/primitives'; +import { Badge } from '@/components/ui/badge'; +import type { CustomComponent } from '@/types'; + +/** + * Molecule — the base kit + custom components list with mapping status, + * isolation triggers and the .tsx upload intake. Store-free. + */ +export function LibraryList({ + customComps, + onIsolate, + onUpload, +}: { + customComps: CustomComponent[]; + onIsolate: (type: string) => void; + onUpload: (file: File) => void; +}) { + return ( +
+ BASE KIT — MAPPED TO YOUR CODE + {LIBRARY.map((item) => ( +
+ + {NODE_GLYPHS[item.type]} + + {item.name} + + + {'primitive' in item && item.primitive ? 'html' : 'unmapped' in item && item.unmapped ? 'unmapped' : '@solstice/ui'} + +
+ ))} + + + + {customComps.length > 0 && CUSTOM — YOUR CODE} + {customComps.map((cc) => ( +
+ +
+ {cc.name} + {cc.file} +
+ +
+ ))} +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/PolishIssueCard.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/PolishIssueCard.tsx new file mode 100644 index 0000000..ca3d3e2 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/PolishIssueCard.tsx @@ -0,0 +1,36 @@ +import { Badge } from '@/components/ui/badge'; +import { cn } from '@/lib/utils'; +import type { PolishIssue } from '@/types'; + +const sevTone = { HIGH: 'danger', MED: 'warn', LOW: 'neutral' } as const; + +/** Molecule — one ranked craft issue with its why (teach) and one-click fix. */ +export function PolishIssueCard({ issue, onFix }: { issue: PolishIssue; onFix: () => void }) { + return ( +
+
+ {issue.sev} + + {issue.title} + +
+

{issue.why}

+ {issue.fixed ? ( + ✓ Fixed — in your tokens + ) : ( + + )} +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/SegmentedControl.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/SegmentedControl.tsx new file mode 100644 index 0000000..eb2502e --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/SegmentedControl.tsx @@ -0,0 +1,65 @@ +import { cn } from '@/lib/utils'; + +export interface SegmentedOption { + value: T; + label: string; +} + +/** Molecule — engraved segmented control (chip well + raised active seg). */ +export function SegmentedControl({ + options, + value, + onChange, + className, +}: { + options: SegmentedOption[]; + value: T; + onChange: (v: T) => void; + className?: string; +}) { + return ( +
+ {options.map((opt) => { + const active = opt.value === value; + return ( + + ); + })} +
+ ); +} + +/** Molecule — dial-like numeric stepper for token-bound values. */ +export function Stepper({ + value, + onStep, + format = (v) => `${v}px`, +}: { + value: number; + onStep: (delta: 1 | -1) => void; + format?: (v: number) => string; +}) { + const btn = + 'flex h-5 w-5 items-center justify-center rounded-[2px] border border-line-2 bg-raised text-[12px] text-ink-2 hover:text-ink'; + return ( +
+ + {format(value)} + +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/TokenEditor.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/TokenEditor.tsx new file mode 100644 index 0000000..315054a --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/TokenEditor.tsx @@ -0,0 +1,38 @@ +import { MonoLabel, Swatch } from '@/components/atoms/primitives'; +import type { DesignSystem } from '@/types'; + +const BRAND_OPTIONS = ['#E4572E', '#2A6FDB', '#1F8A5B', '#111827']; + +/** + * Molecule — the token editor: brand ramp + curated swatches + usage. + * Store-free; the parent owns the system and the ripple. + */ +export function TokenEditor({ + system, + usedCount, + onBrand, +}: { + system: DesignSystem; + usedCount: number; + onBrand: (hex: string) => void; +}) { + return ( +
+ COLOR — BRAND +
+ {([100, 300, 500, 700, 900] as const).map((k) => ( + + ))} +
+
+ {BRAND_OPTIONS.map((hex) => ( + + ))} +
+ brand-500 · used by {usedCount} components + RADIUS — {system.radius}px · TYPE — {system.font} +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/molecules/ZoomControls.tsx b/docs/product/reference/project/veect-codebase/src/components/molecules/ZoomControls.tsx new file mode 100644 index 0000000..95cf014 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/molecules/ZoomControls.tsx @@ -0,0 +1,40 @@ +import { useReactFlow, useViewport } from '@xyflow/react'; + +const btn = + 'px-2 py-1 font-mono text-[10.5px] text-ink-2 hover:bg-white/5 hover:text-ink'; + +/** + * Molecule — board zoom cluster, wired to the React Flow viewport. + * Must render inside to reach its context. + */ +export function ZoomControls() { + const { zoomIn, zoomOut, zoomTo, fitView } = useReactFlow(); + const { zoom } = useViewport(); + + return ( +
+
+ + + +
+ +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/organisms/ActivityBar.tsx b/docs/product/reference/project/veect-codebase/src/components/organisms/ActivityBar.tsx new file mode 100644 index 0000000..bb9de5a --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/organisms/ActivityBar.tsx @@ -0,0 +1,123 @@ +import type { ReactNode } from 'react'; +import { cn } from '@/lib/utils'; +import { useVeect } from '@/store/veect'; + +const Icon = ({ d, extra }: { d: string[]; extra?: ReactNode }) => ( + + {d.map((p, i) => ( + + ))} + {extra} + +); + +const ICONS: Record = { + chat: , + layers: , + library: ( + } + /> + ), + tokens: ( + + + + + } + /> + ), + inspect: ( + + + + + } + /> + ), + polish: , + code: , +}; + +interface Item { + key: string; + label: string; + icon: ReactNode; + active: boolean; + onClick: () => void; +} + +/** + * Organism — the leftmost activity bar: every panel is a toggleable + * icon, grouped by dividers (chat · board panels · detail panels · code). + */ +export function ActivityBar() { + const { chatOpen, boardRailCollapsed, inspectorCollapsed, codeOpen, setPanel } = useVeect(); + const boardTab = useVeect((s) => s.boardTab); + const setBoardTab = useVeect((s) => s.setBoardTab); + const inspTab = useVeect((s) => s.inspTab); + const setInspTab = useVeect((s) => s.setInspTab); + + const boardShown = !boardRailCollapsed; + const inspShown = !inspectorCollapsed; + + const openBoard = (tab: typeof boardTab) => () => { + if (boardShown && boardTab === tab) setPanel('board', false); + else { + setBoardTab(tab); + setPanel('board', true); + } + }; + const openInsp = (tab: typeof inspTab) => () => { + if (inspShown && inspTab === tab) setPanel('inspector', false); + else { + setInspTab(tab); + setPanel('inspector', true); + } + }; + + const groups: Item[][] = [ + [{ key: 'chat', label: 'Chat', icon: ICONS.chat, active: chatOpen, onClick: () => setPanel('chat', !chatOpen) }], + [ + { key: 'layers', label: 'Layers', icon: ICONS.layers, active: boardShown && boardTab === 'layers', onClick: openBoard('layers') }, + { key: 'library', label: 'Library', icon: ICONS.library, active: boardShown && boardTab === 'library', onClick: openBoard('library') }, + { key: 'tokens', label: 'Tokens', icon: ICONS.tokens, active: boardShown && boardTab === 'tokens', onClick: openBoard('tokens') }, + ], + [ + { key: 'inspect', label: 'Inspect', icon: ICONS.inspect, active: inspShown && inspTab === 'inspector', onClick: openInsp('inspector') }, + { key: 'polish', label: 'Polish', icon: ICONS.polish, active: inspShown && inspTab === 'polish', onClick: openInsp('polish') }, + ], + [{ key: 'code', label: 'Code', icon: ICONS.code, active: codeOpen, onClick: () => setPanel('code', !codeOpen) }], + ]; + + return ( + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/organisms/BoardCanvas.tsx b/docs/product/reference/project/veect-codebase/src/components/organisms/BoardCanvas.tsx new file mode 100644 index 0000000..66388a7 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/organisms/BoardCanvas.tsx @@ -0,0 +1,87 @@ +import { useCallback, useEffect, useState } from 'react'; +import { + applyNodeChanges, + Background, + BackgroundVariant, + ReactFlow, + type Node, + type NodeChange, +} from '@xyflow/react'; +import '@xyflow/react/dist/style.css'; +import { FrameNode } from '@/components/organisms/FrameNode'; +import { ZoomControls } from '@/components/molecules/ZoomControls'; +import { findNode, frames } from '@/engine/tree'; +import { useVeect } from '@/store/veect'; +import type { VeectNode } from '@/types'; + +const nodeTypes = { frame: FrameNode }; + +const toFlowNodes = (board: VeectNode): Node[] => + frames(board).map((frame) => ({ + id: frame.id, + type: 'frame', + position: { x: frame.props.x ?? 0, y: frame.props.y ?? 0 }, + data: { frame }, + dragHandle: '.frame-drag-handle', + })); + +/** + * Organism — the infinite board on React Flow: scroll pans, ⌘/pinch + * zooms at the cursor, empty-pane drag pans, pane click deselects. + * The zustand tree stays the single source of truth — React Flow only + * carries frame positions and the viewport. + */ +export function BoardCanvas() { + const board = useVeect((s) => s.board); + const select = useVeect((s) => s.select); + const mutate = useVeect((s) => s.mutate); + const [nodes, setNodes] = useState(() => toFlowNodes(board)); + + // resync from the document store (add/delete/breakpoint/undo) + useEffect(() => setNodes(toFlowNodes(board)), [board]); + + const onNodesChange = useCallback( + (changes: NodeChange[]) => setNodes((nds) => applyNodeChanges(changes, nds)), + [], + ); + + const onNodeDragStop = useCallback( + (_e: unknown, node: Node) => { + mutate((draft) => { + const frame = findNode(draft, node.id); + if (!frame) return; + frame.props.x = Math.round(node.position.x); + frame.props.y = Math.round(node.position.y); + }, 'Move frame'); + }, + [mutate], + ); + + return ( +
+ select(node.id)} + onPaneClick={() => select(null)} + fitView + fitViewOptions={{ padding: 0.18, maxZoom: 1 }} + minZoom={0.08} + maxZoom={2.5} + panOnScroll + zoomOnScroll={false} + zoomOnPinch + panOnDrag={[0, 1]} + nodesFocusable={false} + deleteKeyCode={null} + className="!bg-bg" + > + + + +
+ ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/organisms/BoardSidebar.tsx b/docs/product/reference/project/veect-codebase/src/components/organisms/BoardSidebar.tsx new file mode 100644 index 0000000..0f96460 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/organisms/BoardSidebar.tsx @@ -0,0 +1,83 @@ +import { LayerTree } from '@/components/molecules/LayerTree'; +import { LibraryList } from '@/components/molecules/LibraryList'; +import { TokenEditor } from '@/components/molecules/TokenEditor'; +import { CollapseButton } from '@/components/molecules/CollapseButton'; +import { MonoLabel } from '@/components/atoms/primitives'; +import { ramp } from '@/engine/color'; +import { parseComponentSource } from '@/engine/upload'; +import { frames, walk } from '@/engine/tree'; +import { withFrameViews } from '@/engine/views'; +import { useVeect } from '@/store/veect'; + +const TITLES = { layers: 'LAYERS', library: 'LIBRARY — YOUR SYSTEM', tokens: 'TOKENS' } as const; + +/** + * Organism — the board rail. The activity bar picks the tab; this is + * a thin shell around store-free molecules. + */ +export function BoardSidebar() { + const tab = useVeect((s) => s.boardTab); + const board = useVeect((s) => s.board); + const system = useVeect((s) => s.system); + const setSystem = useVeect((s) => s.setSystem); + const select = useVeect((s) => s.select); + const selectedId = useVeect((s) => s.selectedId); + const setPanel = useVeect((s) => s.setPanel); + const setIsolate = useVeect((s) => s.setIsolate); + const setInspTab = useVeect((s) => s.setInspTab); + const customComps = useVeect((s) => s.customComps); + const addCustomComp = useVeect((s) => s.addCustomComp); + const mutate = useVeect((s) => s.mutate); + + let brandUse = 0; + walk(board, (n) => { + if (['button', 'badge', 'avatar'].includes(n.type)) brandUse += 1; + }); + + return ( + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/organisms/ChatPanel.tsx b/docs/product/reference/project/veect-codebase/src/components/organisms/ChatPanel.tsx new file mode 100644 index 0000000..87074de --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/organisms/ChatPanel.tsx @@ -0,0 +1,66 @@ +import { MonoLabel } from '@/components/atoms/primitives'; +import { ChatBubble } from '@/components/molecules/ChatBubble'; +import { ChatComposer } from '@/components/molecules/ChatComposer'; +import { CollapseButton } from '@/components/molecules/CollapseButton'; +import { Button } from '@/components/ui/button'; +import { useAiCompose } from '@/hooks/useAiCompose'; +import { useVeect } from '@/store/veect'; + +/** + * Organism — the vertical chat column. Layout only: the constrained-AI + * orchestration lives in useAiCompose, the composer is a molecule. + */ +export function ChatPanel() { + const chat = useVeect((s) => s.chat); + const model = useVeect((s) => s.model); + const setModel = useVeect((s) => s.setModel); + const setPanel = useVeect((s) => s.setPanel); + const { submit, accept, discard, thinking, proposal, targetNode, clearTarget } = useAiCompose(); + + return ( + + ); +} diff --git a/docs/product/reference/project/veect-codebase/src/components/organisms/CodePanel.tsx b/docs/product/reference/project/veect-codebase/src/components/organisms/CodePanel.tsx new file mode 100644 index 0000000..740c8d0 --- /dev/null +++ b/docs/product/reference/project/veect-codebase/src/components/organisms/CodePanel.tsx @@ -0,0 +1,118 @@ +import { useMemo, useState } from 'react'; +import { CollapseButton } from '@/components/molecules/CollapseButton'; +import { buildCode } from '@/engine/codegen'; +import { parseGeneratedTsx } from '@/engine/parser'; +import { CUSTOMER_LIB } from '@/config/demo'; +import { useActiveFrame, useVeect, findNode } from '@/store/veect'; + +/** + * Organism — the live code companion. Deterministic tree → TSX with the + * customer's real imports; click a line to select its node. Edit mode + * round-trips: the same grammar parses back onto the canvas. + */ +export function CodePanel() { + const system = useVeect((s) => s.system); + const select = useVeect((s) => s.select); + const selectedId = useVeect((s) => s.selectedId); + const setPanel = useVeect((s) => s.setPanel); + const mutate = useVeect((s) => s.mutate); + const customComps = useVeect((s) => s.customComps); + const frame = useActiveFrame(); + + const [editing, setEditing] = useState(false); + const [draft, setDraft] = useState(''); + const [error, setError] = useState(null); + + const { lines } = useMemo( + () => (frame ? buildCode(frame, system, false, customComps) : { lines: [] }), + [frame, system, customComps], + ); + const text = useMemo(() => lines.map((l) => l.t).join('\n'), [lines]); + + const startEdit = () => { + setDraft(text); + setError(null); + setEditing(true); + }; + + const apply = () => { + if (!frame) return; + const result = parseGeneratedTsx(draft, customComps); + if ('error' in result) { + setError(`Line ${result.line}: couldn't read “${result.error}” — the editor speaks the generated grammar.`); + return; + } + mutate((board) => { + const f = findNode(board, frame.id); + if (!f) return; + f.children = result.children; + if (result.name) f.name = result.name; + }, 'Edited code by hand'); + setEditing(false); + setError(null); + }; + + return ( +