Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm run typecheck # tsc --noEmit on @opencanva/core
```

- The demo app runs the CLI directly: `opencanva <dev|build|preview|sync|init>` (defaults to `dev`). Regenerate the committed skills with **`npm run sync`** (it runs in `apps/demo`); bare `opencanva sync` inside this monorepo is refused (it would write a stray skills tree).
- `npm run sync` copies the skills bundled in `packages/core/skills/` into the demo workspace's **`.agents/skills/`** (vendor-neutral, committed) **and `.claude/skills/`** (Claude-specific, git-ignored): `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme`.
- `npm run sync` copies the skills bundled in `packages/core/skills/` into the demo workspace's **`.agents/skills/`** (vendor-neutral, committed) **and `.claude/skills/`** (Claude-specific, git-ignored): `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme`, `svg-illustration`.
- `opencanva init [dir]` scaffolds a fresh OpenCanva project (config, starter design, and both skill dirs) so a new project is multi-agent ready out of the box.
- After changing `designPresets` in `packages/core/src/design.ts` (every preset needs `vibe` + `tags`), run **`npm run gen:catalog && npm run sync`** — it regenerates the theme catalog table inside the `create-design` skill from the presets; `check:sync` fails CI if the table is stale.
- To typecheck the designs specifically: `npx tsc --noEmit -p apps/demo` (this is what CI runs alongside the core typecheck).
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ npm run typecheck # tsc --noEmit on @opencanva/core
```

- The demo app runs the CLI directly: `opencanva <dev|build|preview|sync|init>` (defaults to `dev`). Regenerate the committed skills with **`npm run sync`** (it runs in `apps/demo`); bare `opencanva sync` inside this monorepo is refused (it would write a stray skills tree).
- `npm run sync` copies the skills bundled in `packages/core/skills/` into the demo workspace's **`.agents/skills/`** (vendor-neutral, committed) **and `.claude/skills/`** (Claude-specific, git-ignored): `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme`.
- `npm run sync` copies the skills bundled in `packages/core/skills/` into the demo workspace's **`.agents/skills/`** (vendor-neutral, committed) **and `.claude/skills/`** (Claude-specific, git-ignored): `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme`, `svg-illustration`.
- `opencanva init [dir]` scaffolds a fresh OpenCanva project (config, starter design, and both skill dirs) so a new project is multi-agent ready out of the box.
- After changing `designPresets` in `packages/core/src/design.ts` (every preset needs `vibe` + `tags`), run **`npm run gen:catalog && npm run sync`** — it regenerates the theme catalog table inside the `create-design` skill from the presets; `check:sync` fails CI if the table is stale.
- To typecheck the designs specifically: `npx tsc --noEmit -p apps/demo` (this is what CI runs alongside the core typecheck).
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,15 @@ export default [Poster] satisfies Scene[];
```

- `export default` is `Scene[]` — one component per artboard (most designs have one; several = a carousel / size set).
- Objects: `<Box>`, `<Text>`, `<Ellipse>`, `<Line>`, `<ImageObject>`, `<Group>`, `<Icon>` — positioned with `x/y/w/h`, rotated with `rotate`, themed with `var(--ox-*)` tokens.
- Objects: `<Box>`, `<Text>`, `<Ellipse>`, `<Line>`, `<ImageObject>`, `<Group>`, `<Icon>`, `<Illustration>` — positioned with `x/y/w/h`, rotated with `rotate`, themed with `var(--ox-*)` tokens.
- Themes: `ember`, `noir`, `sunset`, `mint`, `blueprint`, `bubblegum` (see `/themes`), or roll your own `DesignSystem`.

## Why it's agent-native

- **Click-to-source.** Every object you click resolves to the exact `<Box>`/`<Text>` in your source (via injected `data-ox-loc` read off the React fiber — works across files and components). Drag/resize/recolor rewrite the object's props **in place** through a dev-only write-back API; the source stays byte-stable so HMR is surgical.
- **Comments → edits.** Leave a `@canva-comment` on any object; the bundled `apply-comments` skill applies them.
- **Current-design pointer.** The dev server writes `.opencanva/current.json` so an agent can resolve "this design / the object I selected".
- **Works with any agent.** A root `AGENTS.md` (the cross-tool standard read by Codex, Cursor, Copilot, …) carries the repo guide, and `CLAUDE.md` is a byte-identical copy of it (kept in sync by CI). The authoring skills — `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme` — ship as Agent Skills: `npm run sync` copies them into both `.agents/skills/` (vendor-neutral, committed) and `.claude/skills/` (local). `opencanva init` scaffolds a new project with all of it baked in.
- **Works with any agent.** A root `AGENTS.md` (the cross-tool standard read by Codex, Cursor, Copilot, …) carries the repo guide, and `CLAUDE.md` is a byte-identical copy of it (kept in sync by CI). The authoring skills — `canva-authoring`, `create-design`, `apply-comments`, `current-design`, `create-theme`, `svg-illustration` — ship as Agent Skills: `npm run sync` copies them into both `.agents/skills/` (vendor-neutral, committed) and `.claude/skills/` (local). `opencanva init` scaffolds a new project with all of it baked in.

## Layout

Expand Down
2 changes: 1 addition & 1 deletion apps/demo/.agents/skills/apply-comments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The comment text is the string after `@canva-comment:` (or the `data-ox-comment`
## Workflow

1. **Collect** every marker with `grep` (above). Note the file, line, and the object each marker is attached to.
2. **Apply edits bottom-up** — process markers in **descending line order within each file** so earlier edits don't shift later line numbers. For each: read the surrounding code, make the change the comment asks for (consult `canva-authoring` for how — adjust props like `x`/`y`/`w`/`h`/`size`/`fill`/`color`, swap a token, restructure objects), following the design's existing palette and style.
2. **Apply edits bottom-up** — process markers in **descending line order within each file** so earlier edits don't shift later line numbers. For each: read the surrounding code, make the change the comment asks for (consult `canva-authoring` for how — adjust props like `x`/`y`/`w`/`h`/`size`/`fill`/`color`/`background`, swap a token, restructure objects), following the design's existing palette and style.
3. **Remove the marker** you just satisfied — delete the `{/* @canva-comment: … */}` child or the `data-ox-comment="…"` attribute.
4. **Verify none remain**:
```bash
Expand Down
3 changes: 2 additions & 1 deletion apps/demo/.agents/skills/canva-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ Every object takes `x, y` (position), `w, h` (size), and optional `rotate` (deg,
| `<Line>` | `w` (length), `thickness`, `color`, `dash`, `rotate` | Dividers, rules, accents. |
| `<ImageObject>` | `src`, `fit` (`cover`\|`contain`), `radius` | Photos/logos. Put files in `designs/<id>/assets/` and use `src="./assets/photo.jpg"`. |
| `<Group>` | (position only) | A transparent container for a **logical unit** (a card's parts, a labelled stat, a header cluster). Move/resize/rotate it as one object; the editor can select it as a unit, and multi-select → **Group**/**Ungroup** wraps or dissolves one. Children position **relative to the group**. |
| `<Icon>` | `glyph` (emoji/char), `size`, `color` | Emoji or a single glyph. Pass SVG as children for custom marks. |
| `<Icon>` | `glyph` (emoji/char), `size`, `color` | Emoji or a single glyph, or a small custom SVG mark as children — legacy for anything larger. |
| `<Illustration>` | `background` (mat color behind the SVG) | A full-bleed host for a hand-authored inline SVG illustration. Read the **`svg-illustration`** skill before using this. |

## Authoring rules (these keep the inspector working)

Expand Down
149 changes: 149 additions & 0 deletions apps/demo/.agents/skills/svg-illustration/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
name: svg-illustration
description: Use when authoring a hand-drawn SVG illustration, vector art, or a scene/character graphic for an OpenCanva design — "draw an illustration of X", "make a vector graphic", "illustrate this concept", "add a hero illustration". Not for diagrams, flowcharts, or dividers — those use the existing Box/Line/Text primitives per canva-authoring.
---

# Draw an SVG illustration

This skill covers organic/illustrative vector art — a hero graphic, a scene, a character, a decorative
mark bigger than a simple glyph. Diagrams, flowcharts, and dividers stay with `Box`/`Line`/`Text` per
**`canva-authoring`** — that's a different discipline (layout math), not this one (composition/lighting
technique).

## Hosting

Use `<Illustration>` (from `@opencanva/core`), not `<Icon>`:

```tsx
import { useId } from 'react';
import { Illustration } from '@opencanva/core';

function CoffeeCup() {
const uid = useId().replace(/:/g, '');
return (
<svg width="100%" height="100%" viewBox="0 0 200 200">
<defs>
<linearGradient id={`${uid}-cup`} x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stopColor="var(--ox-accent)" />
<stop offset="100%" stopColor="color-mix(in srgb, var(--ox-accent) 60%, #1a1a4e 40%)" />
</linearGradient>
</defs>
<rect x="40" y="60" width="120" height="100" rx="12" fill={`url(#${uid}-cup)`} />
</svg>
);
}

<Illustration x={120} y={200} w={300} h={300} name="Coffee cup illustration">
<CoffeeCup />
</Illustration>
```

- The inner `<svg>` always declares `width="100%" height="100%"` and its own `viewBox="0 0 W H"` — an
internal coordinate space independent of the object's outer `w`/`h` (artboard pixels). The board's
stylesheet also sizes `.ox-illustration > svg` to fill the box by default, but set the attributes
anyway — it's the documented contract, not a fallback to lean on.
- Set `name="…"` for a real layers-panel label.
- **If you factor the illustration into a reusable component** (the pattern `canva-authoring` teaches
for repeated shapes), its return value must be a single root `<svg>` — not wrapped in a `<div>`, not
a `Fragment` with siblings. The sizing rule above targets a *direct* SVG child; a wrapper silently
defeats it, with nothing to signal why the illustration renders at the wrong size.
- **The inspector's fill swatch on an `<Illustration>` colors the "Background" mat behind the SVG, not
the artwork itself** — visible only if the SVG has transparent margins. Recolor the illustration by
editing its own `fill`/`stroke`/`stop-color`/`color-mix()` values directly, not through the swatch.

## This is JSX, not raw SVG

Multi-word attributes are camelCase — `stopColor`, `strokeLinecap`, `strokeWidth`, `patternUnits`,
`clipPath` — never kebab-case (`stop-color`, `stroke-linecap`), and `class` is `className`. Most SVG
reference material online is written in raw-SVG kebab-case; copying it verbatim fails `tsc`.

## Every `<defs>` id must be unique per rendered instance

Gradients, masks, `clipPath`s, and filters are referenced by `url(#id)`. A hardcoded id collides the
moment the same illustration renders twice — a reusable component used more than once in one design,
or this app's own live-canvas-plus-thumbnail double render. Call `useId()` once at the top of the
component and interpolate it into every id, stripping the colons `useId()` includes by default (safe
as a plain attribute value, but not worth depending on being safe inside a `url(#…)` reference too):

```tsx
import { useId } from 'react';
const uid = useId().replace(/:/g, '');
// id={`${uid}-sky`} ... fill={`url(#${uid}-sky)`}
```

## No external references, scripts, or event handlers; no nested `<foreignObject>`

No `<image href="https://…">`, no `@import`, no `<script>`, no inline `onclick`/etc. External
references are already known to break the rasterized PNG/PDF export path; scripts/handlers have no
purpose in a static export and are a plain security smell; a nested `foreignObject` risks colliding
with the one the export pipeline already wraps the whole board in. This is authoring guidance, not
something the primitive enforces — designs are trusted source, not sanitized input.

## Filter bleed vs. the object's own box

A glow, drop-shadow, or blur filter commonly paints outside the geometric bounds of the shape it's
applied to — that's normal. Two different edges can clip it, with two different fixes:

- **The SVG's own viewport edge.** Expand the `viewBox` to include the bleed margin (content nominally
at `0,0`–`400,400` needs `viewBox="-20 -20 440 440"` for a 20px blur margin).
- **The `<Illustration>` object's own `w`/`h` on the artboard.** Expanding the viewBox alone keeps the
bleed *inside* the object's existing frame — it does not make it visually extend past the frame into
the surrounding canvas. If that's the goal, size the object itself larger than the visual subject and
center the subject with margin inside it, the same "budget the box for what you want visible"
principle `canva-authoring` applies at the artboard edge, just one level down.

## Build workflow

1. Block out the composition with primitive shapes (`rect`, `ellipse`, `polygon`) before adding path
detail — don't start with freehand bezier curves on a blank canvas.
2. Render and look — a separate, explicit step, not blended into "build it well": open the design in
the dev server, actually view it, and run `window.__ox.lint()`.
3. Fix by editing the SVG in place with targeted edits; batch every fix spotted in one look into a
single pass before re-rendering.
4. A full-bleed illustration touching its container edges is expected — the lint's crowding check
exempts illustrations by design, the same way it exempts `Ellipse`/`Image`. Don't shrink a correct
composition chasing a warning that isn't firing.
5. **The lint does not check illustrations for occlusion yet, in either direction** (hidden behind
another object, or hiding one). Visually confirm both, every time — this is one thing automated
checking won't catch for you.

## Technique

- Multi-stop gradients (4+ stops, hue-shifted) — a flat 2-stop gradient reads as flat.
- A few tonal zones for volume: highlight → base → shadow. Shadows shift cooler/darker, never pure
black.
- Organic or character shapes: build in a fixed order (torso → limbs → head), checking after each
addition — not all at once.
- A scene is layered back-to-front (far → near), not painted in one flat pass.

## Theme integration

Fills, strokes, and gradient stops reference `var(--ox-accent)` etc. so the illustration restyles with
the theme, the same as every other primitive. The tonal range the technique above needs
(highlight/shadow variants) comes from the theme's one flat accent via `color-mix()`, not a separate
hardcoded palette:

```
fill="color-mix(in srgb, var(--ox-accent) 70%, white)" /* highlight */
fill="color-mix(in srgb, var(--ox-accent) 60%, #1a1a4e 40%)" /* cooled shadow */
```

This resolves identically live and exported — the export pipeline computes styles the same way for
every existing `var(--ox-*)` usage.

## Two styles

- **Rich/layered** (default for a hero illustration) — gradients, lighting, soft shadows, as above.
- **Flat/geometric** (small marks) — grid-snapped shapes, arcs and straight lines only, no gradients.

No separate catalogue — the palette itself already comes from the design's theme; pick the style by
role (hero vs. small mark), not by browsing options.

## Self-review

- Rendered and viewed at real size; `window.__ox.lint()` is clean (a full-bleed crowding warning would
be a bug in the lint, not your composition — it's exempted by design).
- Colors are theme tokens or `color-mix()` derivations, not hardcoded hex.
- No stray meta-text (echoed prompt, style name) baked into the artwork.
- Content stays inside its `viewBox`; nothing hidden behind or hiding another object, checked by eye.
- Every `<defs>` id is `useId()`-derived, not a static string.
2 changes: 1 addition & 1 deletion packages/core/skills/apply-comments/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ The comment text is the string after `@canva-comment:` (or the `data-ox-comment`
## Workflow

1. **Collect** every marker with `grep` (above). Note the file, line, and the object each marker is attached to.
2. **Apply edits bottom-up** — process markers in **descending line order within each file** so earlier edits don't shift later line numbers. For each: read the surrounding code, make the change the comment asks for (consult `canva-authoring` for how — adjust props like `x`/`y`/`w`/`h`/`size`/`fill`/`color`, swap a token, restructure objects), following the design's existing palette and style.
2. **Apply edits bottom-up** — process markers in **descending line order within each file** so earlier edits don't shift later line numbers. For each: read the surrounding code, make the change the comment asks for (consult `canva-authoring` for how — adjust props like `x`/`y`/`w`/`h`/`size`/`fill`/`color`/`background`, swap a token, restructure objects), following the design's existing palette and style.
3. **Remove the marker** you just satisfied — delete the `{/* @canva-comment: … */}` child or the `data-ox-comment="…"` attribute.
4. **Verify none remain**:
```bash
Expand Down
3 changes: 2 additions & 1 deletion packages/core/skills/canva-authoring/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ Every object takes `x, y` (position), `w, h` (size), and optional `rotate` (deg,
| `<Line>` | `w` (length), `thickness`, `color`, `dash`, `rotate` | Dividers, rules, accents. |
| `<ImageObject>` | `src`, `fit` (`cover`\|`contain`), `radius` | Photos/logos. Put files in `designs/<id>/assets/` and use `src="./assets/photo.jpg"`. |
| `<Group>` | (position only) | A transparent container for a **logical unit** (a card's parts, a labelled stat, a header cluster). Move/resize/rotate it as one object; the editor can select it as a unit, and multi-select → **Group**/**Ungroup** wraps or dissolves one. Children position **relative to the group**. |
| `<Icon>` | `glyph` (emoji/char), `size`, `color` | Emoji or a single glyph. Pass SVG as children for custom marks. |
| `<Icon>` | `glyph` (emoji/char), `size`, `color` | Emoji or a single glyph, or a small custom SVG mark as children — legacy for anything larger. |
| `<Illustration>` | `background` (mat color behind the SVG) | A full-bleed host for a hand-authored inline SVG illustration. Read the **`svg-illustration`** skill before using this. |

## Authoring rules (these keep the inspector working)

Expand Down
Loading
Loading