72 Commits

Author SHA1 Message Date
Marcos Paulo 87c9c3a151 fix: repair release test failures
site / publish (push) Has been cancelled
release / site (push) Has been cancelled
release / test (push) Has been cancelled
release / windows (push) Has been cancelled
release / macos (push) Has been cancelled
release / linux (push) Has been cancelled
release / publish (push) Has been cancelled
2026-10-29 15:55:21 -03:00
Marcos Paulo ddd6fd96b8 Merge pull request #1 from nxtime/claude/affectionate-euler-blm40d
feat(site): add a landing page published to the pages branch
2026-10-29 15:40:10 -03:00
Claude 944311cba1 feat(site): add a landing page published to the pages branch
The page in site/ reads the latest GitHub release from release.json, offers
the build for the visitor's system, and shows the demo tour, a live agent
run and the feature set on the app's acrylic look. site.yml builds it with
the newest release and commits the result to the pages branch; release.yml
calls it after publishing, and a daily run catches releases made by hand.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J9PiV8m3jnQjxY1skZUJfF
2026-10-29 18:23:31 +00:00
Marcos Paulo bc0c3c1a1b chore(release): 0.9.1
release / test (push) Has been cancelled
release / windows (push) Has been cancelled
release / macos (push) Has been cancelled
release / linux (push) Has been cancelled
release / publish (push) Has been cancelled
2026-10-29 14:37:34 -03:00
Marcos Paulo 23e0571b2c feat: expand agent chat and design handoff 2026-10-29 10:28:15 -03:00
Marcos Paulo 24c2c2ad5f chore(release): 0.9.0
release / test (push) Has been cancelled
release / windows (push) Has been cancelled
release / macos (push) Has been cancelled
release / linux (push) Has been cancelled
release / publish (push) Has been cancelled
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:12 -03:00
Marcos Paulo 1b056f0f01 perf: cut chat and image memory, wrap long answer chips
Chat screenshots move to files (kept 14 days), the history list holds no
messages, thumbnails stay small, canvas images decode only for the GPU and
leave it when idle, and long answer options wrap instead of widening the chat.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 13503d4c19 feat(mcp): wire the new tools and document them
export_presentation, critique_design, design_lessons, claim_nodes,
release_claims, list_claims and undo/redo scope; 94 tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo a9a719f1ef feat(ui): page icons, a searchable pages list, sliding layer buttons
Pages can carry an icon, the pages popover filters by name, a layer row's eye
and lock slide in only while shown, and agent outlines fade sooner.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo e4639054e7 feat(mcp): let several agents share one project safely
Named CLI agents (--as, --parent), conflict reports and ifRev on writes,
claims that fence layers off, delegate build scopes and per-agent undo.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 264629ed47 feat(mcp): critique designs through reviewer personas and keep approved lessons
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 5428122609 feat(screens): keep a screen's device height and add sections
A screen made at a device width keeps that device's height and scrolls what
does not fit. Sections group screens on the canvas, drawn as a faint outline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 8d3306ea1f feat(ui-kits): add shadcn, MUI, Mantine, Bootstrap and Chakra kits
The four derived kits reuse the shadcn markup with their own tokens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo b64a779619 feat(mcp): make write_html follow the browser more closely
Rewrites common unsupported CSS into what the canvas has, makes room when a
frame grows into the ones below it, keeps growing text in hugging rows at its
width, and lints what still differs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 975dcfe023 feat(io): export the screens as a presentation deck
A PDF with a cover, contents and a slide per screen, from the File menu, the
palette, the inspector's Export section and `--format presentation`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 74ba94b66f fix(web): give config_dir a wasm variant so the web build compiles
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:47:02 -03:00
Marcos Paulo 580fa9ecc3 docs(skills): teach the incremental check loop in the papercraft skill
The skill's lint step now says to lint only what changed between edits
(rev, changedOnly, since), to read geometry with get_bounds and changes with
get_changes, and to finish with one full lint and a screenshot per mode. The
several-agents section uses get_changes {exclude: "self"}, and the cheat
sheet lists both new tools and the overflow rule. This replaces the pending
snippet file.

The reference and docs/agents.md now match the tools as merged: lint_design
reports coverage under `covered`, and get_bounds returns columns and rows
with continueWith when truncated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:53:27 -03:00
Marcos Paulo 27e6765a43 docs: mind the build machine and keep full test runs on CI
RULES.md gains "The machine you build on": check memory, load and running
builds first, cap Cargo jobs and test threads, run at low priority, one heavy
job at a time, stop when the person says the machine is slow, and clean up
scratch targets. Locally only the test targets a change touches run; the full
suite runs on GitHub before a release. AGENTS.md, CONTRIBUTING.md and the PR
checklist say the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:50:31 -03:00
Marcos Paulo 9de7c4d8ae ci: run the full test suite before every release
test.yml runs cargo test and the wasm check on a Windows runner. The release
workflow calls it and publishes only when it passes; it can also be run on
its own with `gh workflow run test.yml -f ref=<branch>`. Like the release
jobs, it only runs on github.com.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:50:31 -03:00
Marcos Paulo a98bed0da7 test: pass tool arguments to log_agent in the UI smoke test
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:50:07 -03:00
Marcos Paulo e876035fc6 fix: reset the change log on every project switch
Editor::load replaced the change log with a fresh one, so rev restarted at 0
when a project was opened from the app. Once the new project caught up, an
agent's old `since` looked current and get_changes reported a partial list
instead of `truncated`. The log now carries across load and resets there,
which covers the app, the session and MCP alike.

The MCP-only reset after create_file goes: that tool creates a project
without opening it. Tests now open the project with open_file and read the
rev before writing instead of assuming the log starts at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 18:50:07 -03:00
Marcos Paulo f4fd3a8607 docs: count 88 tools after get_changes and get_bounds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 18:10:05 -03:00
Marcos Paulo 83c644d2bf Merge branch 'worktree-agent-a8158d642a885966f' into integration/incremental-lint 2026-10-28 18:09:56 -03:00
Marcos Paulo 1d4eb97d98 Merge branch 'worktree-agent-aaa0fa0d394a3b41c' into integration/incremental-lint 2026-10-28 18:09:55 -03:00
Marcos Paulo a7302326f2 Merge branch 'worktree-agent-afce846aed3239911' into integration/incremental-lint
# Conflicts:
#	PROGRESS.md
2026-10-28 18:09:55 -03:00
Marcos Paulo 72931c5220 Merge branch 'worktree-agent-ae1f23fd3f4aeb50f' into integration/incremental-lint 2026-10-28 18:09:46 -03:00
Marcos Paulo b848eabdf3 feat: incremental lint with a change log and get_changes
A bounded log (10000 layer changes, each tagged with the client that made it)
records what every commit, undo and redo touched; variable edits count as
touching the layers bound to them. Write tools return rev, get_basic_info
reports it, get_changes lists changes (exclude: self, by), and lint_design
takes since / changedOnly to check only changed layers plus what they affect.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 18:09:27 -03:00
Marcos Paulo cd2913d6f7 feat: add the overflow lint rule and the get_bounds tool
lint_design flags layers past their artboard or clipping parent and text
taller than its box (scrolling frames exempt). get_bounds returns the
rects of whole subtrees in one read.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 18:05:54 -03:00
Marcos Paulo ec6da1d383 fix: treat agent reads as reads in the pill, outlines and follow camera
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 17:43:14 -03:00
Marcos Paulo ccc6da2e8c docs(specs): design swarm safety for concurrent agents
Identity, optimistic concurrency, soft claims, per-client undo, offline multi-process locking, test harness and phases.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 17:42:45 -03:00
Marcos Paulo 39933d3c74 docs: add incremental checks workflow (rev, get_changes, scoped lint, get_bounds)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 17:34:24 -03:00
Marcos Paulo 5ee037a7c3 docs: add an AI usage policy, AGENTS.md and CODEOWNERS
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 16:33:24 -03:00
Marcos Paulo d97b889455 docs: add contributing guide, project rules, code of conduct and security policy
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 16:19:19 -03:00
Marcos Paulo 65d160af3b ci: install libdbus and the Apple silicon target for release builds
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 16:01:32 -03:00
Marcos Paulo 837c4448b0 ci: build and publish releases on GitHub only
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 14:27:54 -03:00
Marcos Paulo db650b3ea6 docs: bring the README status and known limits up to date
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 14:20:08 -03:00
Marcos Paulo 76afc98890 docs: drop the paper-rs fork credit and upstream references
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 12:40:25 -03:00
Marcos Paulo 8d46a01a10 chore(release): 0.8.4
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 12:30:22 -03:00
Marcos Paulo 7a6d664c42 docs(readme): add demo video and refresh screenshots
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 12:25:45 -03:00
Marcos Paulo 70830478d9 fix(ui): read the performance overlay's prefs from the project config dir
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 12:25:44 -03:00
Marcos Paulo c575b7e7a7 feat(ui): menu icons and offsets, panel grips, / to hide panels, narrow-window fixes
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 10:04:38 -03:00
Marcos Paulo ec7f1f8a14 feat(ui): refresh-rate frame cap, Task Manager-accurate RAM/CPU, menu toggle and themed close button
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 08:55:37 -03:00
Marcos Paulo 24f6c797f2 chore(release): 0.8.1
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 01:16:28 -03:00
Marcos Paulo a2885a3283 perf(ui): cache the Design tab's lint and feedback until the document changes
The tab ran the design lint over every node, and scanned every node for open
comments, on every frame. On a 12.6k-node page that cost hundreds of ms per
frame (377 ms worst) while the tab was open. Both results are now kept per
(doc_stamp, active page); the open-feedback count of other pages refreshes once
a second.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 01:14:34 -03:00
Marcos Paulo 0143f1e465 feat(perf): RAM and CPU on Windows; refresh rates up to 360 Hz
The performance overlay showed n/a for RAM and CPU on Windows: the readings
existed only for macOS and Linux. They now come from GetProcessMemoryInfo
(working set) and GetProcessTimes (kernel plus user time).

The "Warn below" chips become "Refresh rate" and now go up to 144, 165, 240
and 360, for high-refresh monitors. The default stays 60.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-28 01:14:34 -03:00
Marcos Paulo dc57f9af20 feat(ui): window buttons in the toolbar on Windows/Linux; controls and acrylic from craft-ui
- Drop the separate title bar: minimize/maximize/close sit at the toolbar's right,
  its empty middle drags the window (double-click maximizes). Home gets the same.
- Linux is undecorated too, with edge resize.
- Settings pages fill the modal height and scroll (Appearance rendered empty).
- Checkbox, toggle, select and the acrylic material now come from craft-ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 00:44:02 -03:00
Marcos Paulo 984ede7d55 chore(release): 0.8.0
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:57:42 -03:00
Marcos Paulo fc398bd1f9 feat: screens, packed pages, per-layer change log and canvas perf
- Screens: frames can be declared screens or overlays (set_screens, inspector toggles,
  "Declare N screens"). Agent edits to layers hidden behind a drawer/modal in a screen
  are refused unless allowHidden; partly covered ones are reported. get_basic_info lists
  screens, get_node_info reports visibility, lint skips hidden layers, On click lists
  screens, nested screens get canvas labels. Spec in specs/screens.
- Pages and versions are stored as zstd-compressed compact JSON (.json.zst, format v3);
  legacy .json still reads and is replaced on save.
- Editor keeps a per-layer change log; the scene cache re-tessellates only changed layers
  and no longer keeps a copy of every node (less RAM).
- Canvas perf: zoom settle and LOD, 24-byte vertices, vsync-paced redraws, idle hover
  skip, malloc relief when idle, off-screen frame labels skipped, layer-panel heights
  memoized across frames, run recount on a background thread.
- Frame labels: no label on tiny frames, overlapping labels dropped, ellipsis.
- Selects: menu sizes to content (capped), path labels keep the name, scroll, tooltips.
- Settings: performance monitor page, agent logos, chat page scroll fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 22:55:43 -03:00
Marcos Paulo 75faf3efaa feat(ui): acrylic material, appearance options, chat polish, perf monitor; enforcer fixes
Appearance: acrylic material with popup and modal backing, corner radius,
UI font (bundled DM Sans, Figtree, Fredoka, Geist, Nunito, Quicksand,
Varela Round), accent colour. Icons on buttons, code targets and agent
logos; redesigned fill/gradient and auto-layout rows; compact topbar.

Chat: agent logo in the header, history popup with search and recency
groups, new-chat button, issue outcomes saved with the chat and issue
bodies shown as fields, question cards with one-click answers, number
keys, an inline other-answer field and an answered state.

Layers: right-click context menu. Performance: virtualised layer reveal,
pruned hit-testing, glyph cache. Settings > Performance: FPS, frame time,
RAM, CPU and GPU overlay with a slow-frame warning.

MCP: compact spec writes bound strokes as variables (#23), lint contrast
averages gradient and translucent backgrounds (#24), lint flags light/dark
theme copies (#25).

Closes #23, closes #24, closes #25

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 18:02:23 -03:00
Marcos Paulo 4f5dd1f399 install-local: build in the repo's target dir; keep only the newest app backup
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 18:22:33 -03:00
Marcos Paulo 033ae23b7c feat(enforcer): Tailwind v4 utilities (bg-primary, text-muted-foreground/80) count as token references
verify_code flagged every shadcn/Tailwind screen as hard-coded because it only looked for
var(--name) and --color-name in the code text.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 17:42:43 -03:00
Marcos Paulo a0be70d5a6 fix(enforcer): verify_code no longer panics on multibyte layer names in the code
strip_token_declaration_blocks sliced rest[..n] on a byte index that could fall inside a
multibyte char; a screen whose labels say "Rótulo" took the app down mid-call. Regression test
with a ~10 KB multi-file React module.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 17:40:04 -03:00
Marcos Paulo ef29427877 feat(mcp): compact design spec and project inventory for design-to-code pipelines
get_design_spec {format:"compact"} renders one line per node with tokens used, kit
components, component definitions and notes; no reference code or per-node enforcement
prose. About 12x fewer tokens than the JSON spec (a 1440x960 screen: ~7k vs ~93k).

get_inventory returns every page's screens with a size class (mobile, tablet, desktop,
email, pdf-a4), component/instance/annotation counts, notes still marked `? (team)`,
every component and the token group sizes in one call.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-25 17:05:02 -03:00
Marcos Paulo 2af0155e6a feat(props modal): tabs, table and highlighted source views, variable colours; overlays under modals
The properties modal gets a tab per property, a Table view (colour cells
pick project variables, visibility toggles, icon, image and text fields)
and a Source view with JSON highlighting, neutral styling and one text
size. Canvas overlays move below modals; segmented controls key ids by
their labels so two in one row both respond. The status template writes
known colours as variables.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:21:23 -03:00
Marcos Paulo 8130c5cc68 fix(merge_variants): moved instances keep their nested instances' own values
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:09:35 -03:00
Marcos Paulo 67d23a229f fix(props): a nested instance's properties are found through the outer main
A badge inside a row links to a layer of the row's main; its properties
live on the component that layer is an instance of. Follow the links.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 16:05:03 -03:00
Marcos Paulo 6c9515c385 feat: properties modal for main components; simpler inline list
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:58:01 -03:00
Marcos Paulo d4f12f6773 fix(merge_variants): moved instances keep their texts; options carry nested instance state
An instance moved from a variant kept only its overrides, so texts that
came from the variant turned into the base's. They are restored now
(except state texts). A nested instance's property values (a badge's
Tone) are compared and become props in the options.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:52:40 -03:00
Marcos Paulo 65a70996c4 feat(merge_variants): textLayers, so only state texts (a badge label) join the options
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 15:46:12 -03:00
Marcos Paulo 2b8443a1b1 feat: set_prototype_links for agents; merge_variants compares icon colours
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 14:06:48 -03:00
Marcos Paulo c169058915 fix: keep legacy nested replacements as swaps on sync; icons drawn once at their size
- Sync converted away an instance written over a nested one (older
  storage: its own root, no swap) and lost it; it now becomes a swap of
  the main's child at that place.
- The parser lowercases attribute names, so icon and plugin code never
  dropped the SVG's own viewBox; with the viewBox fit that scaled icons
  twice. Compare names without case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:49:06 -03:00
Marcos Paulo 4c2003d05e fix(merge_variants): empty frames' arrangement and flex-shrink do not block a merge
An empty base takes the variant's arrangement when it gains that variant's
layers, so a check lands centred like in the variant.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:38:07 -03:00
Marcos Paulo 42bb758383 feat(merge_variants): grow the base with hidden layers, extend an existing enum, refuse one-option merges
Layers a variant has and the base lacks are copied into the base hidden,
and that option shows them. Icons are compared as one unit. A base that
already has the enum keeps its options and gains the new ones; its
instances keep their values. Fewer than two mergeable variants changes
nothing. Removing a property clears its values on instances.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:29:07 -03:00
Marcos Paulo 26dab9b26b fix(merge_variants): leave out variants that differ in layout, size or spacing
Such differences cannot be an option's changes and would be lost; the
variant stays as it was and the warning names the properties. A main's
own canvas position is ignored.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:20:27 -03:00
Marcos Paulo d1a4129323 feat: merge_variants; absolute children stay pinned in scrolling screens
merge_variants turns per-state components into one with an enum property:
variant differences by layer path become options, instances on every page
move to the base with their value, emptied variants are deleted. Absolute
children of a scrolling screen stay put while its content scrolls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:13:41 -03:00
Marcos Paulo a283ddf457 fix(layout): hug children re-measure after a stretch gives them their width
A hug frame whose width comes from its parent kept the height measured
while its text was narrow (1404px instead of 84px). After arranging, a
stale hug child lays out again and the frame measures once more.
Closes #22.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:05:12 -03:00
Marcos Paulo b172762288 feat: component properties (enum, text, bool, icon) with templates
Main components define properties; instances keep values as overrides, so
main edits keep them and definition changes re-apply everywhere. Enum
options use the overrides format and can restyle, retext, show, hide or
re-icon any layer. Templates: text, active (tab bars, navs), status.
set_component_props, set_instance_props, props= on <x-paper-instance>,
get_components lists them, {Prop} in code links, inspector controls.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 13:01:34 -03:00
Marcos Paulo b3da7e84d8 feat: scrolling screens: fixed size, overflow scrolls on the canvas and in Present
A frame's scroll flag (overflow: auto|scroll) keeps it fixed height and
clipped; the wheel over it scrolls its content, then pans when the content
ends. The offset is view state and flows through parent_origin, so hit
tests and overlays follow. Toggles beside each screen, on a selected frame
and for every screen on the page. Agents keep screens at their size.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:44:47 -03:00
Marcos Paulo 96989be616 fix: borders over clipped children, border variables stay bound, fit-content around growing children
- A clipping frame draws its border after its children. Closes #19.
- border: ... var(--x) binds the stroke; reads show var(--x); find_nodes
  says when it stopped at 500. Closes #20.
- Hug frames grow around flex-grow children; a min height stays and the
  growing child fills it. Closes #21.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:33:48 -03:00
Marcos Paulo 9514f15073 feat: follow agents on the canvas; skeletons while an agent writes
- Settings, Chat, Follow agents (on by default): the canvas pans and
  switches page to the layer any agent changes, in both modes.
- Claude Code and the API announce a tool call as it starts streaming
  (ToolDraft, ToolInput); the canvas shows a skeleton over its target, or
  where a new artboard goes, until it has run. New agent content comes in
  through a fading skeleton, so an outline never frames empty space.
- Top bar: the title and agent pill leave room measured from the right-hand
  buttons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 12:21:09 -03:00
Marcos Paulo 1a57eccd38 fix(lint): report and bind hard-coded borders that equal a color variable
lint_design warns on a border whose hex equals a color variable, and
fix: true binds it (stroke), so switching the variable mode reaches it.
Closes #18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:58:55 -03:00
Marcos Paulo fdfd82207d fix: svg viewBox fit, notes off narrow screens, icon-aware not-an-instance, instance text width
- write_html fits an <svg> viewBox into its width and height. Closes #14.
- A note whose target sits straight in its artboard treats the artboard as
  the screen, so tidy keeps cards off it. Closes #15.
- not-an-instance needs the same icon, and its fix names the
  <x-paper-instance> to write. Closes #16.
- Instances with their own text re-measure it when the main changes.
  Closes #17.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-25 03:51:06 -03:00
250 changed files with 22346 additions and 2052 deletions
+1 -1
View File
@@ -1,4 +1,4 @@
# Upstream (michaelshimeles/paper-rs) carries no ArtCraft/Discord branding; keep it that way. Run: perl tools/craft-skills/skills/debrand/debrand.pl --check .
# The tree carries no ArtCraft/Discord branding; keep it that way. Run: perl tools/craft-skills/skills/debrand/debrand.pl --check .
keep ^(LICENSE-(MIT|APACHE)|\.debrand)$
forbid (?i)artcraft
forbid (?i)discord
+1
View File
@@ -0,0 +1 @@
* @nxtime
+19
View File
@@ -0,0 +1,19 @@
---
name: Bug report
about: Something does not work as expected
labels: bug
---
**What happened**
**What you expected**
**Steps to reproduce**
1.
2.
**Version and platform**
PaperCraft version (the release you installed), OS, and the GPU if it is a rendering problem.
**Screenshot, recording or project**
Attach one if you can. Remove anything private first.
+5
View File
@@ -0,0 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Security vulnerability
url: https://github.com/nxtime/papercraft/security/advisories/new
about: Report security problems privately, not as an issue.
+14
View File
@@ -0,0 +1,14 @@
---
name: Feature request
about: Suggest an idea or a missing capability
labels: enhancement
---
**The problem**
What are you trying to do, and what gets in the way?
**What you would like**
**Alternatives you considered**
**Is it already in ROADMAP.md?**
+24
View File
@@ -0,0 +1,24 @@
## What and why
<!-- What changed, and the problem it solves. Link the issue: Fixes #123 -->
## How I tested it
<!-- Tests added, commands run, platforms tried. -->
## AI assistance
<!-- Which tool helped and how much, or "none". See AI_POLICY.md. You must understand every line. -->
## Screenshots
<!-- For UI changes: before and after, at scale 2, in each theme. -->
## Checklist
- [ ] Tests first; new code is covered, error paths included
- [ ] `cargo fmt --all`, `cargo clippy --all-targets` and the affected `cargo test --test` targets pass
- [ ] Follows [RULES.md](../RULES.md) (no overflow, data safety, one undo step per tool call)
- [ ] Docs, tool descriptions and `PROGRESS.md` updated where needed
- [ ] Conventional commit messages
- [ ] I read and understand every line of this change (AI-assisted or not)
+101
View File
@@ -0,0 +1,101 @@
# Builds Windows, macOS and Linux on GitHub's runners when a v* tag is pushed, then publishes a
# GitHub release with the files once the full test suite (test.yml) passes, then refreshes the
# landing page (site.yml). Gitea reads .github/workflows too, so every job is gated on the
# server URL: this pipeline only ever runs on github.com.
name: release
on:
push:
tags: ["v*"]
workflow_dispatch:
inputs:
tag:
description: Existing tag to build and release (e.g. v0.8.4)
required: true
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
TAG: ${{ inputs.tag || github.ref_name }}
jobs:
test:
uses: ./.github/workflows/test.yml
with: { ref: "${{ inputs.tag || github.ref }}" }
windows:
if: github.server_url == 'https://github.com'
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with: { ref: "${{ inputs.tag || github.ref }}" }
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build and zip
shell: pwsh
run: |
cargo build --release --locked --bins
$v = $env:TAG.TrimStart('v')
New-Item -ItemType Directory -Force dist | Out-Null
$exes = 'papercraft','papercraft-cli','papercraft-mcp','papercraft-relay' | ForEach-Object { "target/release/$_.exe" }
Compress-Archive -Path $exes -DestinationPath "dist/papercraft-$v-windows-x86_64.zip"
- uses: actions/upload-artifact@v4
with: { name: windows, path: dist/* }
macos:
if: github.server_url == 'https://github.com'
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with: { ref: "${{ inputs.tag || github.ref }}" }
- uses: dtolnay/rust-toolchain@stable
with: { targets: aarch64-apple-darwin }
- uses: Swatinem/rust-cache@v2
- name: Package (Apple silicon, ad-hoc signed, not notarized)
run: packaging/macos/package.sh --arch aarch64
- uses: actions/upload-artifact@v4
with: { name: macos, path: dist/release/* }
linux:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { ref: "${{ inputs.tag || github.ref }}" }
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Build dependencies
run: |
sudo apt-get update
sudo apt-get install -y pkg-config libssl-dev libxkbcommon-x11-dev libxcb-xkb-dev libx11-xcb-dev libwayland-dev libvulkan-dev libdbus-1-dev
- name: Build and tar
run: |
cargo build --release --locked --bins
v="${TAG#v}"
mkdir -p dist stage
cp target/release/papercraft target/release/papercraft-cli target/release/papercraft-mcp target/release/papercraft-relay stage/
tar -C stage -czf "dist/papercraft-$v-linux-x86_64.tar.gz" .
tar -C stage -czf "dist/papercraft-cli-$v-linux-x86_64.tar.gz" papercraft-cli papercraft-mcp
- uses: actions/upload-artifact@v4
with: { name: linux, path: dist/* }
publish:
if: github.server_url == 'https://github.com'
needs: [test, windows, macos, linux]
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with: { path: files, merge-multiple: true }
- name: Create the release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create "$TAG" files/* --repo "$GITHUB_REPOSITORY" --title "PaperCraft ${TAG#v}" \
--generate-notes --verify-tag || gh release upload "$TAG" files/* --repo "$GITHUB_REPOSITORY" --clobber
site:
if: github.server_url == 'https://github.com'
needs: publish
uses: ./.github/workflows/site.yml
+69
View File
@@ -0,0 +1,69 @@
# Builds the landing page (site/) with the latest GitHub release and commits it to the `pages`
# branch, which GitHub Pages serves (Settings → Pages → Deploy from a branch → pages, / (root)).
# release.yml calls it after publishing, a daily run catches releases made by hand, and pushes to
# the page's sources rebuild it. Nothing is committed when the result is unchanged. Gitea reads
# .github/workflows too, so the job is gated on the server URL.
name: site
on:
workflow_call:
workflow_dispatch:
schedule:
- cron: "23 5 * * *"
push:
branches: [main]
paths:
- site/**
- docs/demo.mp4
- docs/screenshots/readme/**
- assets/app-icon/papercraft.svg
- assets/agents/**
- .github/workflows/site.yml
permissions:
contents: write
concurrency:
group: site
cancel-in-progress: false
jobs:
publish:
if: github.server_url == 'https://github.com'
runs-on: ubuntu-latest
steps:
# The page comes from the default branch even when a release tag calls this workflow.
- uses: actions/checkout@v4
with: { ref: "${{ github.event.repository.default_branch }}" }
- name: Install ffmpeg
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends ffmpeg
- name: Check out the pages branch
run: |
echo "PAGES=$RUNNER_TEMP/pages" >> "$GITHUB_ENV"
PAGES="$RUNNER_TEMP/pages"
if git ls-remote --exit-code --heads origin pages > /dev/null; then
git fetch --depth 1 origin pages:refs/remotes/origin/pages
git worktree add --detach "$PAGES" origin/pages
else
git worktree add --orphan -b pages "$PAGES"
fi
- name: Build
env:
GH_TOKEN: ${{ github.token }}
run: sh site/build.sh dist/site "$PAGES"
- name: Commit and push
run: |
cd "${PAGES:?}"
# Clearing through git drops files the new build no longer has, and only tracked ones.
git rm -rqf --ignore-unmatch .
cp -a "$GITHUB_WORKSPACE/dist/site/." .
git add -A
if git diff --cached --quiet; then
echo "The page is already up to date."
exit 0
fi
tag="$(jq -r .tag release.json)"
git -c user.name="github-actions[bot]" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -q -m "chore(site): publish the page for $tag"
git push origin HEAD:refs/heads/pages
+34
View File
@@ -0,0 +1,34 @@
# The full test suite. Local runs cover only the targets a change touches (RULES.md, "The machine
# you build on"); this is where everything runs, before a release can publish. Run it alone with
# `gh workflow run test.yml -f ref=<branch or tag>`. Gitea reads .github/workflows too, so the job
# is gated on the server URL.
name: test
on:
workflow_call:
inputs:
ref:
type: string
required: false
workflow_dispatch:
inputs:
ref:
description: Branch or tag to test (default the one you run from)
required: false
env:
CARGO_TERM_COLOR: always
jobs:
test:
if: github.server_url == 'https://github.com'
# Windows: the platform the app is developed on, and its runners render with a software GPU.
runs-on: windows-latest
steps:
- uses: actions/checkout@v4
with: { ref: "${{ inputs.ref || github.ref }}" }
- uses: dtolnay/rust-toolchain@stable
with: { targets: wasm32-unknown-unknown }
- uses: Swatinem/rust-cache@v2
- run: cargo test --locked --no-fail-fast
- run: cargo check --locked --lib --target wasm32-unknown-unknown
@@ -0,0 +1,2 @@
[ 3482ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 3531ms] [LOG] [HMR] connected @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
@@ -0,0 +1,9 @@
[ 210ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 1039ms] [LOG] [Fast Refresh] done in 930ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 1076ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 1086ms] [LOG] [HMR] connected @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 37298ms] [ERROR] Pattern attribute value [0-9() -]{10,16} is not a valid regular expression: Uncaught SyntaxError: Invalid regular expression: /[0-9() -]{10,16}/v: Invalid character in character class @ http://localhost:3000/motorista/entrar:0
[ 60872ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 60976ms] [LOG] [Fast Refresh] done in 99ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 61021ms] [LOG] [Fast Refresh] done in 99ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 66490ms] [LOG] [Fast Refresh] done in 99ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
@@ -0,0 +1,13 @@
[ 93ms] [LOG] [Fast Refresh] done in 99ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 953ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 963ms] [LOG] [HMR] connected @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 4345ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 4433ms] [LOG] [Fast Refresh] done in 189ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 4548ms] [VERBOSE] [DOM] Password forms should have (optionally hidden) username fields for accessibility: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:3000/entrar/senha:0
[ 9731ms] [LOG] [Fast Refresh] done in 5486ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 9777ms] [LOG] [Fast Refresh] done in 5486ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 11070ms] [LOG] [Fast Refresh] done in 5486ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 30758ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=fBVPvwqt_6CU4TU3WWYmu' failed: @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 31764ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=fBVPvwqt_6CU4TU3WWYmu' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 32766ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=fBVPvwqt_6CU4TU3WWYmu' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 33776ms] [LOG] [HMR] connected @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
@@ -0,0 +1,79 @@
[ 805ms] [INFO] %cDownload the React DevTools for a better development experience: https://react.dev/link/react-devtools font-weight:bold @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 813ms] [LOG] [HMR] connected @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 1121ms] [VERBOSE] [DOM] Password forms should have (optionally hidden) username fields for accessibility: (More info: https://goo.gl/9p2vKq) %o @ http://localhost:3000/entrar/senha:0
[ 2267ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 2310ms] [LOG] [Fast Refresh] done in 144ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 9621ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 9763ms] [LOG] [Fast Refresh] done in 243ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 48693ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 48749ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 48826ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 51025ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 51539ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 51907ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 107213ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 118297ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 123464ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 125669ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 125771ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 126799ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 126941ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 127929ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 127981ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 154930ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 156620ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 157845ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158106ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158454ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158504ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158748ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158848ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 158955ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 159307ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 159399ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 159488ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 159901ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 159993ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 187894ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 188000ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 205603ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 207208ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 207296ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 207951ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208126ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208244ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208353ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208415ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208590ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208695ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208794ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208878ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 208986ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 209132ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 209234ms] [LOG] [Fast Refresh] done in 39171ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 228278ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 228308ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 238991ms] [LOG] [Fast Refresh] done in 10713ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 239037ms] [LOG] [Fast Refresh] done in 10713ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 239175ms] [LOG] [Fast Refresh] rebuilding @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 239206ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 239602ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 239697ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 264111ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 265916ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 321743ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 333352ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 369410ms] [LOG] [Fast Refresh] done in 3ms @ http://localhost:3000/_next/static/chunks/1bba_next_dist_041c6vio7na_y._.js:719
[ 413886ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 414902ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 415904ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 416906ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 417908ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 418911ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 423918ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 428928ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 433950ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 438984ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 444022ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 449184ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
[ 454479ms] [ERROR] WebSocket connection to 'ws://localhost:3000/_next/hmr?id=ruCdDO_vey1tElDs8WYR0' failed: Error in connection establishment: net::ERR_CONNECTION_REFUSED @ http://localhost:3000/_next/static/chunks/1bba_next_dist_client_0ru2jkh0js1s8._.js:16225
@@ -0,0 +1,28 @@
- generic [ref=e2]:
- complementary [ref=e3]:
- generic [ref=e8]:
- generic [ref=e9]: Rota Fresca
- generic [ref=e10]: Distribuição de alimentos
- generic [ref=e11]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=e12]
- paragraph [ref=e13]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=e14]:
- link "Precisa de ajuda?" [ref=e16] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=e17]:
- generic [ref=e18]:
- heading "Entrar" [level=1] [ref=e19]
- paragraph [ref=e20]: Use o e-mail da sua conta. Conforme o tipo de conta, pedimos a senha ou mandamos um link de acesso.
- generic [ref=e21]:
- generic [ref=e22]:
- generic [ref=e23]: E-mail
- textbox "E-mail" [ref=e24]:
- /placeholder: nome@empresa.com.br
- button "Continuar" [ref=e25]
- generic [ref=e26]: ou
- generic [ref=e33]:
- paragraph [ref=e34]: É motorista?
- paragraph [ref=e35]: Entre pelo app Rota Fresca no celular, com o seu número. Não precisa de e-mail.
- generic [ref=e36]:
- paragraph [ref=e37]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=e38]: Termos de uso · Privacidade
@@ -0,0 +1,20 @@
- generic [active] [ref=f1e1]:
- generic [ref=f1e2]:
- generic [ref=f1e3]:
- generic [ref=f1e8]:
- generic [ref=f1e9]: Rota Fresca
- generic [ref=f1e10]: App do motorista
- banner [ref=f1e11]:
- heading "Entrar" [level=1] [ref=f1e12]
- paragraph [ref=f1e13]: Use o número de celular que a central cadastrou. Mandamos um código, sem senha.
- generic [ref=f1e14]:
- generic [ref=f1e15]: Celular
- generic [ref=f1e16]:
- generic [ref=f1e17]: "+55"
- textbox "(21) 98877-4410" [ref=f1e18]
- generic [ref=f1e19]:
- button "Receber código no WhatsApp" [ref=f1e20]
- button "Receber por SMS" [ref=f1e21]
- paragraph [ref=f1e22]: Trocou de número? Fale com a central.
- button "Open Next.js Dev Tools" [ref=f1e28] [cursor=pointer]
- alert [ref=f1e32]
@@ -0,0 +1,32 @@
- generic [active] [ref=f2e1]:
- generic [ref=f2e2]:
- complementary [ref=f2e3]:
- generic [ref=f2e9]:
- generic [ref=f2e10]: Rota Fresca
- generic [ref=f2e11]: Distribuição de alimentos
- generic [ref=f2e12]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f2e13]
- paragraph [ref=f2e14]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f2e15]:
- link "Precisa de ajuda?" [ref=f2e17] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f2e18]:
- generic [ref=f2e19]:
- heading "Entrar" [level=1] [ref=f2e20]
- paragraph [ref=f2e21]: Use o e-mail da sua conta. Conforme o tipo de conta, pedimos a senha ou mandamos um link de acesso.
- generic [ref=f2e22]:
- generic [ref=f2e23]:
- generic [ref=f2e24]: E-mail
- textbox "E-mail" [ref=f2e25]:
- /placeholder: nome@empresa.com.br
- button "Continuar" [ref=f2e26]
- generic [ref=f2e27]: ou
- button "Entrar com chave de acesso (passkey)" [ref=f2e31]
- generic [ref=f2e35]:
- paragraph [ref=f2e36]: É motorista?
- paragraph [ref=f2e37]: Entre pelo app Rota Fresca no celular, com o seu número. Não precisa de e-mail.
- generic [ref=f2e38]:
- paragraph [ref=f2e39]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f2e40]: Termos de uso · Privacidade
- button "Open Next.js Dev Tools" [ref=f2e46] [cursor=pointer]
- alert [ref=f2e50]
@@ -0,0 +1,38 @@
- generic [active] [ref=f2e1]:
- button "Open Next.js Dev Tools" [ref=f2e46] [cursor=pointer]
- alert [ref=f2e50]: Digite sua senha
- generic [ref=f2e51]:
- complementary [ref=f2e52]:
- generic [ref=f2e58]:
- generic [ref=f2e59]: Rota Fresca
- generic [ref=f2e60]: Distribuição de alimentos
- generic [ref=f2e61]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f2e62]
- paragraph [ref=f2e63]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f2e64]:
- link "Precisa de ajuda?" [ref=f2e66] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f2e67]:
- generic [ref=f2e68]:
- generic [ref=f2e69]: ana.souza@rotafresca.com.br
- button "Trocar" [ref=f2e74]
- generic [ref=f2e75]:
- heading "Digite sua senha" [level=1] [ref=f2e76]
- paragraph [ref=f2e77]: Conta de consultor. Depois da senha pedimos o código do autenticador.
- generic [ref=f2e78]:
- generic [ref=f2e79]:
- generic [ref=f2e80]:
- generic [ref=f2e81]: Senha
- textbox "Senha" [ref=f2e82]
- generic [ref=f2e83]:
- generic [ref=f2e84]:
- checkbox "Mostrar senha" [ref=f2e85]
- checkbox [aria-hidden]
- text: Mostrar senha
- link "Esqueci minha senha" [ref=f2e86] [cursor=pointer]:
- /url: /entrar/redefinir
- button "Entrar" [ref=f2e87]
- paragraph [ref=f2e88]: Pode colar a senha ou usar o gerenciador de senhas do navegador.
- generic [ref=f2e89]:
- paragraph [ref=f2e90]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f2e91]: Termos de uso · Privacidade
@@ -0,0 +1,40 @@
- generic [ref=f2e1]:
- button "Open Next.js Dev Tools" [ref=f2e46] [cursor=pointer]
- alert [ref=f2e50]: Rota Fresca
- generic [ref=f2e51]:
- complementary [ref=f2e52]:
- generic [ref=f2e58]:
- generic [ref=f2e59]: Rota Fresca
- generic [ref=f2e60]: Distribuição de alimentos
- generic [ref=f2e61]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f2e62]
- paragraph [ref=f2e63]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f2e64]:
- link "Precisa de ajuda?" [ref=f2e66] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f2e67]:
- generic [ref=f2e68]:
- generic [ref=f2e69]: ana.souza@rotafresca.com.br
- button "Trocar" [ref=f2e74]
- generic [ref=f2e75]:
- heading "Digite sua senha" [level=1] [ref=f2e76]
- paragraph [ref=f2e77]: Conta de consultor. Depois da senha pedimos o código do autenticador.
- generic [ref=f2e78]:
- generic [ref=f2e79]:
- generic [ref=f2e80]:
- generic [ref=f2e81]: Senha
- textbox "Senha" [active] [invalid] [ref=f2e82]
- generic [ref=f2e83]:
- generic [ref=f2e84]:
- checkbox "Mostrar senha" [ref=f2e85]
- checkbox [aria-hidden]
- text: Mostrar senha
- link "Esqueci minha senha" [ref=f2e86] [cursor=pointer]:
- /url: /entrar/redefinir
- alert [ref=f2e92]:
- generic [ref=f2e93]: Senha incorreta. Confira e tente de novo.
- button "Entrar" [ref=f2e87]
- paragraph [ref=f2e88]: Pode colar a senha ou usar o gerenciador de senhas do navegador.
- generic [ref=f2e89]:
- paragraph [ref=f2e90]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f2e91]: Termos de uso · Privacidade
@@ -0,0 +1,32 @@
- generic [active] [ref=f3e1]:
- generic [ref=f3e2]:
- complementary [ref=f3e3]:
- generic [ref=f3e9]:
- generic [ref=f3e10]: Rota Fresca
- generic [ref=f3e11]: Distribuição de alimentos
- generic [ref=f3e12]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f3e13]
- paragraph [ref=f3e14]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f3e15]:
- link "Precisa de ajuda?" [ref=f3e17] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f3e18]:
- generic [ref=f3e19]:
- heading "Entrar" [level=1] [ref=f3e20]
- paragraph [ref=f3e21]: Use o e-mail da sua conta. Conforme o tipo de conta, pedimos a senha ou mandamos um link de acesso.
- generic [ref=f3e22]:
- generic [ref=f3e23]:
- generic [ref=f3e24]: E-mail
- textbox "E-mail" [ref=f3e25]:
- /placeholder: nome@empresa.com.br
- button "Continuar" [ref=f3e26]
- generic [ref=f3e27]: ou
- button "Entrar com chave de acesso (passkey)" [ref=f3e31]
- generic [ref=f3e35]:
- paragraph [ref=f3e36]: É motorista?
- paragraph [ref=f3e37]: Entre pelo app Rota Fresca no celular, com o seu número. Não precisa de e-mail.
- generic [ref=f3e38]:
- paragraph [ref=f3e39]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f3e40]: Termos de uso · Privacidade
- button "Open Next.js Dev Tools" [ref=f3e46] [cursor=pointer]
- alert [ref=f3e50]
@@ -0,0 +1,38 @@
- generic [active] [ref=f3e1]:
- button "Open Next.js Dev Tools" [ref=f3e46] [cursor=pointer]
- alert [ref=f3e50]
- generic [ref=f3e51]:
- complementary [ref=f3e52]:
- generic [ref=f3e58]:
- generic [ref=f3e59]: Rota Fresca
- generic [ref=f3e60]: Distribuição de alimentos
- generic [ref=f3e61]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f3e62]
- paragraph [ref=f3e63]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f3e64]:
- link "Precisa de ajuda?" [ref=f3e66] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f3e67]:
- generic [ref=f3e68]:
- generic [ref=f3e69]: ana.souza@rotafresca.com.br
- button "Trocar" [ref=f3e74]
- generic [ref=f3e75]:
- heading "Digite sua senha" [level=1] [ref=f3e76]
- paragraph [ref=f3e77]: Conta de consultor. Depois da senha pedimos o código do autenticador.
- generic [ref=f3e78]:
- generic [ref=f3e79]:
- generic [ref=f3e80]:
- generic [ref=f3e81]: Senha
- textbox "Senha" [ref=f3e82]
- generic [ref=f3e83]:
- generic [ref=f3e84]:
- checkbox "Mostrar senha" [ref=f3e85]
- checkbox [aria-hidden]
- text: Mostrar senha
- link "Esqueci minha senha" [ref=f3e86] [cursor=pointer]:
- /url: /entrar/redefinir
- button "Entrar" [ref=f3e87]
- paragraph [ref=f3e88]: Pode colar a senha ou usar o gerenciador de senhas do navegador.
- generic [ref=f3e89]:
- paragraph [ref=f3e90]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f3e91]: Termos de uso · Privacidade
@@ -0,0 +1,33 @@
- generic [active] [ref=f3e1]:
- button "Open Next.js Dev Tools" [ref=f3e46] [cursor=pointer]
- alert [ref=f3e50]
- generic [ref=f3e92]:
- complementary [ref=f3e93]:
- generic [ref=f3e99]:
- generic [ref=f3e100]: Rota Fresca
- generic [ref=f3e101]: Distribuição de alimentos
- generic [ref=f3e102]:
- heading "Do depósito à prateleira, na hora certa." [level=2] [ref=f3e103]
- paragraph [ref=f3e104]: Pedidos, entregas e notas da sua rede no mesmo lugar.
- main [ref=f3e105]:
- link "Precisa de ajuda?" [ref=f3e107] [cursor=pointer]:
- /url: mailto:suporte@rotafresca.com.br
- generic [ref=f3e108]:
- heading "Confirme que é você" [level=1] [ref=f3e113]
- generic [ref=f3e114]:
- generic [ref=f3e115]:
- generic [ref=f3e116]:
- generic [ref=f3e117]: Código
- textbox "Código" [ref=f3e118]
- generic [ref=f3e119]:
- checkbox "Não pedir de novo neste computador por 30 dias" [checked] [ref=f3e120]
- checkbox [checked] [aria-hidden]
- text: Não pedir de novo neste computador por 30 dias
- button "Confirmar" [ref=f3e121]
- generic [ref=f3e122]:
- link "Usar um código de recuperação" [ref=f3e123] [cursor=pointer]:
- /url: /entrar/codigo?recuperacao=1
- paragraph [ref=f3e124]: Perdeu o celular? Peça ao administrador da conta para refazer o autenticador.
- generic [ref=f3e125]:
- paragraph [ref=f3e126]: Recebeu um convite? Abra o link do e-mail para criar seu acesso.
- paragraph [ref=f3e127]: Termos de uso · Privacidade
@@ -0,0 +1,47 @@
- generic [active] [ref=f3e1]:
- button "Open Next.js Dev Tools" [ref=f3e46] [cursor=pointer]
- alert [ref=f3e50]: Bom dia, Ana
- generic [ref=f3e128]:
- complementary [ref=f3e129]:
- generic [ref=f3e130]:
- generic [ref=f3e136]:
- generic [ref=f3e137]: Rota Fresca
- generic [ref=f3e138]: Distribuição de alimentos
- navigation [ref=f3e139]:
- link "Painel" [ref=f3e140] [cursor=pointer]:
- /url: /painel
- link "Roteiro do dia" [ref=f3e143] [cursor=pointer]:
- /url: /roteiro
- link "Lojas (PDVs)" [ref=f3e146] [cursor=pointer]:
- /url: /lojas
- link "Pedidos" [ref=f3e149] [cursor=pointer]:
- /url: /pedidos
- link "Entregas" [ref=f3e152] [cursor=pointer]:
- /url: /entregas
- link "Validade e lotes" [ref=f3e155] [cursor=pointer]:
- /url: /validade
- link "Relatórios" [ref=f3e158] [cursor=pointer]:
- /url: /relatorios
- link "Integrações e notas" [ref=f3e161] [cursor=pointer]:
- /url: /integracoes
- link "Leitura de documentos" [ref=f3e164] [cursor=pointer]:
- /url: /documentos
- link "Contatos e mensagens" [ref=f3e167] [cursor=pointer]:
- /url: /contatos
- link "Configurações" [ref=f3e170] [cursor=pointer]:
- /url: /configuracoes
- generic [ref=f3e173]:
- generic [aria-hidden] [ref=f3e174]: A
- generic [ref=f3e175]:
- generic [ref=f3e176]: Ana Souza
- generic [ref=f3e177]: Consultora · Zona Oeste
- main [ref=f3e178]:
- generic [ref=f3e179]:
- generic [ref=f3e180]:
- heading "Bom dia, Ana" [level=1] [ref=f3e181]
- paragraph [ref=f3e182]: domingo, 25 de outubro
- generic [ref=f3e183]:
- textbox "Buscar loja, pedido ou produto" [ref=f3e187]
- button "Notificações" [ref=f3e188]
- button "Importar / Exportar" [ref=f3e189]
- button "Novo pedido" [ref=f3e190]
Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 344 KiB

+77
View File
@@ -0,0 +1,77 @@
# Guidance for coding agents
PaperCraft is a native Rust design and whiteboard canvas (egui, wgpu, winit) that AI agents drive
over MCP and a CLI. Read [CONTRIBUTING.md](CONTRIBUTING.md) and [RULES.md](RULES.md) first; this
file is the short version for agents, and [AI_POLICY.md](AI_POLICY.md) says what the person using
you is accountable for.
## Commands
```sh
cargo build --release
cargo fmt --all
cargo clippy --all-targets
cargo test --test incremental # only the targets your change touches
cargo test --lib editor::changes # unit tests of one module
cargo check --lib --target wasm32-unknown-unknown
```
Do not run the full suite locally: it builds dozens of test binaries. It runs on GitHub before
each release (`.github/workflows/test.yml`). `tests/update.rs` cannot run on Windows outside CI
(it needs elevation); that failure is known.
Builds run on the person's own machine, so mind its resources (RULES.md, "The machine you build
on"): check free memory and running builds first, cap jobs at no more than half the cores
(`cargo test -j 8 -- --test-threads=8` on 16 cores), run at low priority, run one heavy job at a
time, and stop at once if they say it is slow.
## Running the app safely
Never run a test copy against the person's real projects or MCP port, and never close or restart
their running PaperCraft. Give yours its own folder and port, and keep its PID so you can stop only
your own process:
```sh
PAPERCRAFT_HOME=/tmp/pc PAPERCRAFT_MCP_PORT=29998 ./target/release/papercraft
```
Do not stop processes by name.
## Layout
`src/model` (document), `src/editor` (tools and edits), `src/render` (wgpu), `src/mcp` (tools,
HTML/CSS mapping, lint), `src/ui` (egui panels), `src/io` (import and export), `src/project`
(store and formats), `tests/` (scenario tests), `specs/` (feature specs), `skills/` (agent
skills). The README's project layout lists the rest.
## Conventions
* Tests first; match the style of the tests next to yours, and check the behaviour is not already
covered.
* Changes to the document go through `Editor::edit`: one transaction, one undo step.
* Handle errors; do not `unwrap` user data and do not discard errors with `let _ =`.
* Comments explain *why*, never summarise the code. No dead code, debug prints or
commented-out blocks.
* Files stay under about 800 lines; split by feature.
* Reuse the shared UI kit and theme instead of one-off values. Nothing may overflow its container.
* Writes to projects are atomic and format changes ship a migration (see RULES.md, "Data safety").
* Adding or removing an MCP tool means a test in `tests/mcp.rs`, a precise description, and the
tool count updated in the README and ROADMAP.
* Commits are conventional (`feat(scope): summary`) and focused. Do not commit, push or tag unless
the person asked you to.
## Designing with PaperCraft (skills, examples, screenshots)
* Themes are modes, never copies: one artboard per screen, a value per mode on each variable, and
`set_variable_mode` to switch. Never duplicate artboards for light and dark.
* Every colour on a themed screen is a variable.
* Check each artboard for overflow, programmatically and by screenshot in each mode, and run
`lint_design`.
* Release design locks when you finish.
## Avoid
* Creative additions nobody asked for; stay inside the task.
* Trivial tests, wide or optional types added to appease the compiler, and swallowed errors.
* Editing generated files, lockfiles or release artifacts by hand, or touching files unrelated to
the task. Leave other people's uncommitted work alone.
+40
View File
@@ -0,0 +1,40 @@
# AI usage policy
PaperCraft is built to be driven by AI agents, and many of its contributors use them too. AI tools
are welcome here. What we ask is that a person stays accountable for everything submitted.
## For contributors
* **You own what you submit.** Read every line of your pull request before asking for review. If
you cannot explain what a change does and how it fits the rest of the system without asking an
AI, do not submit it yet.
* **Disclose meaningful AI help.** Say which tool you used and roughly how much it wrote in the
pull request description (for example "drafted with Claude Code, reviewed and tested by me").
Autocomplete and spell-checking do not need a note.
* **Write issues yourself.** An agent can help you research, but the report should be in your own
words, trimmed to what matters, with reproduction steps you ran. Long generated text that nobody
checked puts the burden on maintainers.
* **Think before you generate.** Agents jump to code and make their own job easy: trivial tests,
wide or optional types, swallowed errors, redundant comments, copied local patterns that do not
fit. Push back on all of it. [RULES.md](RULES.md) lists the standards to hold the output to.
* **Verify.** Run the checks in [CONTRIBUTING.md](CONTRIBUTING.md), use the app, and attach
screenshots for UI changes. "The agent said it works" is not a test result.
* **Answer automated review.** If a bot reviews your pull request, fix its findings or reply with
a one-line reason a finding does not apply.
* **No bulk or drive-by pull requests.** Many small generated changes across the tree, or changes
to something nobody asked for, will be closed. Open an issue first for anything beyond a small
fix.
Pull requests that clearly skip these steps may be closed without a detailed review. Repeated
low-effort submissions can lead to a block, under the [Code of Conduct](CODE_OF_CONDUCT.md).
## For coding agents
Read [AGENTS.md](AGENTS.md). It holds the commands, conventions and guardrails an agent working in
this repository should follow.
## Why
Every issue and pull request is read by a person. Review time is the scarce resource, and a change
nobody understood costs more than it saves. AI that is reviewed, tested and understood by the
person submitting it is a good way to build PaperCraft; AI used to skip that work is not.
+1
View File
@@ -0,0 +1 @@
@AGENTS.md
+31
View File
@@ -0,0 +1,31 @@
# Code of Conduct
PaperCraft is built by people with different backgrounds and experience. We want everyone to be
able to contribute without harassment.
## Our standard
Be respectful and constructive. Assume good intent, give specific and kind feedback, and accept
it the same way. Disagree about the work, not the person.
Not acceptable:
* harassment, insults, slurs, or personal or political attacks;
* sexualised language or imagery, or unwelcome attention;
* threats, doxxing, or publishing someone's private information;
* sustained disruption of discussions, or pressure to merge or answer;
* anything else a reasonable person would consider inappropriate in a professional setting.
## Scope
This applies in issues, pull requests, discussions, commit messages and any space where someone
represents the project.
## Enforcement
Maintainers may edit or remove comments, close or lock threads, and block people who break these
rules. To report a problem, use GitHub's "Report content" on the comment, or contact a maintainer
through a private channel on their GitHub profile. Reports are handled privately and in good
faith, and retaliation against a reporter is itself a violation.
The spirit of this text follows the [Contributor Covenant](https://www.contributor-covenant.org).
+129
View File
@@ -0,0 +1,129 @@
# Contributing to PaperCraft
Thanks for wanting to help. PaperCraft is an alpha, so there is plenty to do and the bar for a
first contribution is low: a typo fix, a clearer error message, a missing test or a one-line bug
fix are all welcome.
Please read [RULES.md](RULES.md) before writing code: it holds the standards a change is judged by.
By taking part you agree to the [Code of Conduct](CODE_OF_CONDUCT.md). Using an AI tool is fine;
the [AI usage policy](AI_POLICY.md) says what you stay responsible for, and coding agents should read
[AGENTS.md](AGENTS.md).
## Ways to help
* **Report a bug** or **suggest a feature** with an [issue](https://github.com/nxtime/papercraft/issues/new/choose).
A screenshot or a short recording helps; so does the project folder (or a page of it) that
triggers the bug.
* **Pick something from [ROADMAP.md](ROADMAP.md).** Rows marked `todo` or `partial` are open. Say
which one you are taking in an issue first, so two people do not build the same thing.
* **Improve the docs, skills or screenshots.** [docs/](docs), [skills/](skills) and the README are
as important as code.
* **Review a pull request.** A second pair of eyes is always useful.
Security problems do not go in public issues: see [SECURITY.md](SECURITY.md).
## Before you start
For anything bigger than a small fix, open an issue (or comment on one) describing the problem and
how you plan to solve it. Large features start as a short spec in [specs/](specs): a `spec.md`
(why, what, what is out of scope) and a `plan.md`. [specs/component-props](specs/component-props)
shows the shape. A spec agreed up front saves a rewrite later.
## Set up
You need Rust 1.95 or newer (`rust-version` in `Cargo.toml`) and the platform libraries listed in
the README's [Build from source](README.md#build-from-source).
```sh
git clone https://github.com/nxtime/papercraft
cd papercraft
cargo build --release
```
Run a test copy next to your own PaperCraft so it cannot touch your real projects or collide with
the running app's MCP port:
```sh
PAPERCRAFT_HOME=/tmp/pc PAPERCRAFT_MCP_PORT=29998 ./target/release/papercraft
```
`PAPERCRAFT_HOME` moves the project root and the config folder; `PAPERCRAFT_MCP_PORT` moves the
MCP server. Always set both when developing.
The code depends on [`craft-ui`](https://git.marcospaulo.dev.br/masi/craft-libs), which Cargo
fetches from a public git repository at the revision pinned in `Cargo.toml`.
## Make the change
1. Fork the repository and branch from `main` (`fix/short-name`, `feat/short-name`).
2. **Write the test first.** Fix a bug by adding a test that fails, then make it pass. See
[Tests](#tests).
3. Keep the change focused. One concern per pull request; unrelated cleanups go in their own.
4. Run the checks below and fix what they report.
5. Update the docs the change touches: the README, `docs/`, the tool description for an MCP tool,
the skills if agents should now behave differently.
6. Add a short entry to [PROGRESS.md](PROGRESS.md) for user-visible changes.
### Checks
```sh
cargo fmt --all
cargo clippy --all-targets
cargo test --test <target> # the targets your change touches
cargo check --lib --target wasm32-unknown-unknown # the web build
```
Each file in `tests/` is its own binary linking the whole app, so the full suite is slow and heavy
on a desktop. It runs on GitHub instead: before every release (`.github/workflows/test.yml`), or
on demand with `gh workflow run test.yml -f ref=<branch>`.
Keep the tree free of the third-party branding patterns listed in `.debrand`.
### Tests
* Unit tests sit next to the code in `#[cfg(test)]` modules.
* Scenario tests live in [tests/](tests). They drive the editor with synthetic pointer and
keyboard events; `tests/mcp.rs` covers every MCP tool on a headless session.
* Aim for **80% coverage** on code you add or change, including the error paths.
* GPU-dependent tests skip when no adapter exists. Set `PAPERCRAFT_REQUIRE_GPU=1` to make them
fail instead.
* `tests/update.rs` cannot run on Windows (it needs elevation); that failure is known and not
yours.
* For UI work, run the app, look at it, and attach a screenshot. Headless rendering needs no
display: `papercraft --screenshot ui.png --demo --size 1440x900 --scale 2`.
## Commits
[Conventional commits](https://www.conventionalcommits.org): `type(scope): summary`, with the
types `feat`, `fix`, `refactor`, `docs`, `test`, `chore`, `perf` and `ci`.
```
fix(ui): keep the review row's revert button off the hover edge
feat(mcp): add set_screens
docs(readme): add the demo video
```
Write the summary in the imperative, under about 72 characters, and use the body to say *why*.
Reference the issue (`Fixes #123`).
## Pull requests
* Fill in the template: what changed, why, how you tested it, screenshots for UI changes.
* Keep it reviewable. If it grows past a few hundred lines of non-generated code, split it.
* The branch must be up to date with `main`; rebase rather than merge.
* Expect review comments; they are about the code, not you. Push fixes as new commits and we
squash on merge.
* A maintainer merges once a review is approved.
## Releases
Maintainers cut releases. A `v*` tag pushed to GitHub builds Windows, macOS and Linux and
publishes them (`.github/workflows/release.yml`). Contributors do not need to bump versions.
## Licensing
PaperCraft is dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE). Unless you
state otherwise, anything you submit is licensed under the same terms, with no extra conditions.
Only submit work you wrote or have the right to submit. Code ported from another project needs a
compatible license and an entry in [THIRD_PARTY.md](THIRD_PARTY.md); bundled assets go in
[ATTRIBUTION.md](ATTRIBUTION.md).
Generated
+49 -3
View File
@@ -784,7 +784,7 @@ dependencies = [
[[package]]
name = "craft-ui"
version = "0.1.0"
source = "git+https://git.marcospaulo.dev.br/masi/craft-libs?rev=22544b6d619d4f16779b8bdfa3f0b642a86fc065#22544b6d619d4f16779b8bdfa3f0b642a86fc065"
source = "git+https://git.marcospaulo.dev.br/masi/craft-libs?rev=9c470aa935c4ce48f2ba4f0a17f9410c0c8660fb#9c470aa935c4ce48f2ba4f0a17f9410c0c8660fb"
dependencies = [
"egui",
]
@@ -3128,7 +3128,7 @@ dependencies = [
[[package]]
name = "papercraft"
version = "0.7.3"
version = "0.9.1"
dependencies = [
"arboard",
"axum",
@@ -3161,6 +3161,7 @@ dependencies = [
"resvg",
"rfd",
"rmcp",
"ruzstd",
"semver",
"serde",
"serde_json",
@@ -3174,8 +3175,10 @@ dependencies = [
"web-sys",
"web-time",
"wgpu",
"windows-sys 0.61.2",
"winit",
"winresource",
"zstd",
]
[[package]]
@@ -3853,6 +3856,15 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
[[package]]
name = "ruzstd"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8"
dependencies = [
"twox-hash",
]
[[package]]
name = "ryu"
version = "1.0.23"
@@ -4754,6 +4766,12 @@ dependencies = [
"thiserror 2.0.21",
]
[[package]]
name = "twox-hash"
version = "2.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "86a801b3cea342a06d468c8710662aa29e5e05e4f5c0d62f00bbb7f2ad7941c2"
[[package]]
name = "type-map"
version = "0.5.1"
@@ -4777,7 +4795,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
dependencies = [
"memoffset",
"tempfile",
"windows-sys 0.60.2",
"windows-sys 0.61.2",
]
[[package]]
@@ -6018,6 +6036,34 @@ version = "1.0.23"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
[[package]]
name = "zstd"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64d80649ab6db9d9f6f9c80a40becd948eda4714a0a5ac8c4d157a32231c7882"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.1.2+zstd.1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "481ada9c543b2323cbb7bbdbb5249177fbf0188ed959ed2c15e4b22421c84ed3"
dependencies = [
"cc",
"pkg-config",
]
[[package]]
name = "zune-core"
version = "0.5.3"
+10 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "papercraft"
version = "0.7.3"
version = "0.9.1"
edition = "2024"
rust-version = "1.95"
description = "A native infinite-canvas app for planning, working and sketching"
@@ -44,7 +44,7 @@ env_logger = "0.11"
rfd = "0.17"
fastrand = "2"
dirs = "6"
craft-ui = { version = "0.1.0", git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "22544b6d619d4f16779b8bdfa3f0b642a86fc065" }
craft-ui = { version = "0.1.0", git = "https://git.marcospaulo.dev.br/masi/craft-libs", rev = "9c470aa935c4ce48f2ba4f0a17f9410c0c8660fb" }
i_overlay = "9.0.1"
miniz_oxide = "0.9.1"
usvg = "0.48"
@@ -97,8 +97,14 @@ reqwest = { version = "0.13", default-features = false, features = ["native-tls"
# libc: SIGINT to the spawned chat CLIs.
keyring = { version = "3", default-features = false, features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"] }
libc = "0.2"
# Pages and versions on disk are zstd-compressed JSON (`project::packed`).
zstd = "0.13"
# Browser build (`src/web.rs`): canvas + WebGPU/WebGL surface, DOM events instead of winit.
# Process RAM and CPU readings for the performance overlay (`ui::perf`).
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_System_ProcessStatus", "Win32_System_Threading"] }
[target.'cfg(target_arch = "wasm32")'.dependencies]
wgpu = { version = "30", features = ["webgl"] }
wasm-bindgen = "0.2"
@@ -106,4 +112,6 @@ wasm-bindgen-futures = "0.4"
js-sys = "0.3"
console_error_panic_hook = "0.1"
console_log = "1"
# zstd without C, for the browser: reads the packed files native builds write.
ruzstd = "0.8"
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "KeyboardEvent", "PointerEvent", "WheelEvent", "ClipboardEvent", "DataTransfer", "MediaQueryList", "Location", "Performance", "Navigator", "Clipboard", "CssStyleDeclaration", "console", "WebSocket", "MessageEvent", "CloseEvent", "ErrorEvent", "Storage", "Crypto", "DragEvent", "FileList", "File", "Blob", "Request", "RequestInit", "Response"] }
+123
View File
@@ -76,6 +76,118 @@ are credited in `THIRD_PARTY.md`.
* **Not carried over**: comments, design tokens, AI image generation, video export, teams;
moving layers between pages; remote image URLs (see README Known gaps).
### 2026-10-28: overflow lint rule and `get_bounds`
* `lint_design` has an `overflow` rule (`src/mcp/lint_overflow.rs`): layers past their artboard or clipping parent, and text taller than its box. Scrolling frames and what is pinned in them are exempt; it is never auto-fixed. It is a pure function over a node set, so a scope filter can pass any subset.
* New tool `get_bounds {nodeIds?, pageId?, depth?}` (`src/mcp/bounds.rs`): every layer's rect in one read (about 2000 rows per call, then `continueWith`). Tool count is now 87 (one more than before; if another branch also adds a tool, recount). Tests: `tests/mcp_bounds.rs`.
### 2026-10-28: agent reads no longer look like edits
* Read-only tool calls (`get_node_info`, `get_children`, ...) have honest verbs ("Looking at the design") and light the "Working" pill for 1.5 s instead of 6 s, so it goes out right after a sweep ends.
* A sweep of reads draws at most one layer outline (a read outlines only when no other read ran in the last 2 s) and no longer pans the follow-agent camera; edits behave as before. "Is this a read" comes from `mcp::tools::edits_doc`.
### 2026-10-28: 0.9.0 (presentations, critique, several agents, lighter chats)
* Export the screens as a presentation PDF: cover, contents and a slide per screen (File menu, palette, `--format presentation`, `export_presentation`).
* `critique_design` reviews finished work through personas (vision, UX, flow, QA, visual, accessibility, copy, end-user); `design_lessons` keeps the findings the person approved for every later design.
* Several agents on one project: named CLI agents (`--as`), conflict reports and `ifRev` on writes, `claim_nodes` / `release_claims` / `list_claims`, scoped delegate builds and per-agent undo. 94 tools.
* Screens keep their device height and scroll what does not fit; sections group screens on the canvas; pages can carry icons.
* UI kits for shadcn, MUI, Mantine, Bootstrap and Chakra; `write_html` follows the browser more closely.
* Memory: chat screenshots move to files (kept 14 days), thumbnails stay small, canvas images decode only for the GPU. A long chat dropped from about 1 GB to about 400 MB.
* Fix: long answer options in a question card wrap instead of widening the whole chat.
### 2026-10-28: 0.8.4 (demo video, fresh screenshots)
* README opens with a time-lapse of designing a habit-tracker app (`docs/demo.gif`, full video `docs/demo.mp4`), and every screenshot is retaken from that design.
* Fix: the Performance overlay reads its settings from the project config folder, so `PAPERCRAFT_HOME` isolates it too.
### 2026-10-28: 0.8.3 (menus, panel grips, narrow windows)
* Menus open 6 px below their button, are at least 180 px wide and show an icon beside each item. Below 1100 px the hamburger opens every menu in one popup.
* Side panels: a slim rounded grip in the gap resizes them (double click resets) and `/` hides or shows both. Panels size to their content and step aside when the window is too narrow for the canvas.
* Sharper window buttons (pixel-snapped glyphs); the hamburger morphs into a back arrow while the File / Edit strip slides open and shut.
* Fixes: lint Fix buttons no longer cover the issue text; review rows keep their badge and revert button off the hover edge; the Export button stays on one line.
### 2026-10-28: 0.8.2 (refresh-rate cap, accurate RAM/CPU, toolbar)
* Settings > Performance > Refresh rate is now a real frame cap applied live (30 to 360 Hz, 60 by default); **Auto** draws as fast as the display allows and is the default on macOS.
* Performance Monitor: RAM is the private working set and CPU is divided by logical cores on Windows, so both match Task Manager.
* Windows and Linux toolbar: a hamburger button shows or hides the File / Edit strip (remembered); the close button uses the theme's danger colour on the acrylic surface, with hover animation and a pointing-hand cursor.
### 2026-10-28: 0.8.1 (Performance Monitor on Windows, Design tab speed)
* Performance Monitor: RAM (working set) and CPU now show on Windows; before they were `n/a` there. The overlay and slow-frame badge judge frames against a refresh rate you pick in Settings > Performance: 30 to 360 Hz (144, 165 and 240 included), 60 by default.
* Design tab: the lint and the open-feedback scan are kept until the document changes instead of walking every node each frame; a 12.6k-node page no longer drops to a few FPS while the tab is open.
* Windows and Linux: window buttons in the toolbar; controls and acrylic come from craft-ui.
### 2026-10-27: 0.8.0 (screens, packed pages, canvas performance)
* Screens (`specs/screens`): a frame can be declared a screen or an overlay (drawer, modal, sheet) with `set_screens`, inspector toggles, or Declare N screens on the page. Agent edits to layers hidden behind an overlay or opaque layer in their screen are refused unless `allowHidden`; partly covered ones are reported with their visible share. `get_basic_info` lists screens, `get_node_info` tells a layer's visibility, `lint_design` skips hidden layers, On click lists screens, nested screens get canvas labels.
* Pages and saved versions are stored as zstd-compressed compact JSON (`.json.zst`, project format 3): the 68 MB Interview project is 1.3 MB on disk and loads faster. A legacy `.json` still reads and is replaced on the next save; older builds refuse format 3.
* Canvas performance: the editor logs which layers each change touched, so the scene cache re-tessellates only those and keeps no copy of the document (443 → 346 MB on Interview). Zoom holds its geometry until it settles, tiny layers are skipped, vertices are 24 bytes, redraws follow vsync, the idle pointer no longer redraws, freed memory goes back to the system when idle, off-screen frame labels are skipped, and the layers panel keeps its row heights between frames (panning a large page: ~9 → ~0.7 ms a frame).
* Frame labels: none on frames too narrow on screen, overlapping labels are dropped (selected and larger frames first), long names end in an ellipsis.
* Dropdowns size to their longest option (capped), keep the name of `A / B / Name` options, scroll when long and show the full name on hover.
* Settings: a Performance page (FPS, frame time, RAM, CPU, GPU, optional overlay and slow-frame badge), agent logos in the agent rows; the Chat page no longer collapses.
### 2026-10-25: 0.8.0 (properties modal, row merges)
* Properties modal (inspector, on a main: Edit properties…): a tab per property (name, kind, layer, default, delete); an options property as a Table (a row per option, a column per layer · style; colours picked from the project's variables with a swatch, visibility as a toggle, icon, image and text fields; add or remove options and columns) or as Source (each option's changes as JSON with syntax highlighting); templates from the + Property menu; Save replaces them all. The inline list is name, kind and a remove button. The `status` template writes colours the project has as variables as those variables.
* Canvas overlays (agent glow, agent outlines, review outlines, the agent-elsewhere chip) sit below modals instead of showing through them. Two segmented controls in one row no longer share widget ids (the second did not respond).
* `merge_variants` keeps what each moved instance shows (texts that came from its old variant), carries nested instances' property values (a badge's Tone) in the options, and takes `textLayers` for state texts only.
### 2026-10-25: 0.8.0 (prototype links tool, merge icon colours)
* `set_prototype_links`: agents can link a layer to a screen for Present mode (back arrows, rows, tabs); before, only the inspector could.
* `merge_variants` compares an icon's colour (keeping its variable), now that icons are compared as one unit.
### 2026-10-25: 0.8.0 (data safety fixes)
* Sync no longer throws away an instance written over a nested one the way older versions stored it (its own root, no swap): it becomes a swap of the main's child at that place. Projects built before #11 lost such layers (a checked checkbox, a status badge) on the next edit of the outer main.
* Icons: the HTML parser keeps attribute names in lowercase, so the icon and plugin code never removed the SVG's original `viewBox`; since #14 that made `<x-paper-icon>` (and icon swaps) scale twice and come out tiny. Unreleased builds only; 0.7.3 is not affected.
* `merge_variants` refuses merges that would leave one option and changes nothing then.
### 2026-10-25: 0.8.0 (merge variants, pinned bars)
* `merge_variants`: per-state components (`Tab bar / Hoje`, `Status / Sucesso`...) become one component with an enum property. Each variant's differences from the base, matched by layer path (fills, colours, borders, weight, radius, visibility, icons; texts on request), become its option; every instance on every page moves to the base with its value; the base takes the family name; the emptied variants are deleted. A variant with layers the base lacks is left out and reported.
* Absolute children of a scrolling screen stay pinned while its content scrolls (a tab bar, a floating button), and do not add to the scroll range.
### 2026-10-25: 0.8.0 (fix #22)
* #22: a hug column whose width comes from its parent (stretched or filled) re-measures its height once its text wraps at that width, and the parent measures again, instead of keeping a height measured while the text was narrow.
### 2026-10-25: 0.8.0 (component properties)
* Main components carry properties (`Node.props`): `text`, `bool`, `icon` and `enum`, whose options are changes in the overrides format (a layer name, `$root` or a path `Item > Label`, to a text or styles that may hold `text`, `icon` and `visibility`), so they restyle, retext, show, hide or re-icon any layer. Instances keep their values (`prop_values`) as overrides, so main edits keep them; changing the definitions re-applies every instance on every page.
* Templates: `text` (a property per text layer), `active` (which item of a tab bar, nav or segmented row is active) and `status` (options with a colour tint the root and colour the label and icon).
* `set_component_props`, `set_instance_props`, `props='{...}'` on `<x-paper-instance>`, properties in `get_components`, `{Prop}` in code link templates, and an inspector section (a control per property on an instance; Texts and Active item templates on a main). Icon swaps keep the layer's name and link (a `children` override); recoloured icon shapes inside instances now keep their colour after a sync.
* Spec: `specs/component-props/spec.md`.
### 2026-10-25: 0.8.0 (scrolling screens)
* Screens keep their size and scroll what overflows: a frame's `scroll` flag (CSS `overflow: auto|scroll`, `overflow-y-auto` in the Tailwind export) makes it fixed height and clipped, and the wheel over it scrolls its content on the canvas and in Present, then pans once the content ends. The offset is view state, not saved; hit-testing and overlays follow it.
* Toggles: a scroll button beside each screen's name (shown when it scrolls, overflows, or is hovered), Scroll overflow on a selected frame, and Screens scroll on the page for every screen at once. A fit-content screen that grew goes back to its min height.
* Agents are told to keep screens at their size and use overflow:auto for long content.
### 2026-10-25: 0.8.0 (fixes #19, #20, #21)
* #19: a clipping frame draws its border after its children, so filled rows no longer hide it. Clipping stays rectangular: children's square corners still show past a rounded border.
* #20: a border written with a colour variable (`1px solid var(--border)`) binds the stroke, a literal one unbinds it, and reads show `var(--name)` for bound strokes. `find_nodes` says when it stopped at 500 matches.
* #21: hug (fit-content) frames grow around growing children (CSS flex-basis auto) instead of staying at their min height; with short content the min height stays and the growing child fills it, so a pinned footer works.
### 2026-10-25: 0.8.0 (follow agents, skeletons)
* Follow agents (Settings, Chat; on by default): in Design and Chat mode alike, the canvas pans, and switches page, to the layer any agent changes (the chat, Claude Code over MCP, a helper). Moving the canvas yourself pauses it for a moment; off, a chip offers the agent's page.
* Skeletons: the moment the model starts writing a tool call (`ToolDraft` and `ToolInput` events from Claude Code's stream and the API), the canvas shows a shimmering skeleton over its target, or where a new artboard goes, labelled with what it is doing, until the call has run. Fresh agent content comes in through a skeleton that fades, so an outline never frames empty space.
* Top bar: the title and the agent pill leave room measured from the buttons on the right, so nothing overlaps at narrow widths.
### 2026-10-25: 0.8.0 (fixes #14 to #18)
* #14: `write_html` fits an `<svg>` viewBox into its width and height like a browser (centred, aspect kept).
* #15: a note on something placed straight in its artboard treats the artboard as the screen: tidy keeps cards off it instead of in its free space.
* #16: `not-an-instance` no longer matches look-alikes with a different icon; its fix names the `<x-paper-instance>` to write (swap_instance refuses plain frames).
* #17: instances with their own text re-measure it when the main's text style changes, so hug buttons keep fitting their labels (also covers the size reset after a swap).
* #18: `lint_design` reports borders whose hex equals a color variable, and `fix` binds them (`stroke`), so a mode switch reaches them.
### 2026-10-25: 0.7.3 (QA sub-agent, helper agents, auto mode, mid-turn messages, isolated dev settings)
* QA sub-agent: Claude Code chats always get a Sonnet `qa` agent that lints and screenshots built frames and returns only findings, so screenshots stay out of the main context.
@@ -139,6 +251,17 @@ are credited in `THIRD_PARTY.md`.
* Agent view lists runs and supports review, whole-run revert, per-change undo and accept all. Review adds canvas outlines and a finish toast; palette commands review or revert the last run.
* Id-paired page diffs power review; History version compare UI remains todo. Completed with full restore (including variables, modes and tokens), tool edit classification, and wasm support.
### 2026-10-28: Incremental lint
Agents re-linted whole pages after every edit. A bounded change log (`src/editor/changes.rs`, the
last 10000 layer changes, each with the client that made it) now records which layers every
commit, undo and redo touched; variable edits count as touching the layers bound to them. Write
tools return `rev`, `get_basic_info` reports it, `get_changes {since, page?, exclude?, by?}` lists
the changes, and `lint_design {since | changedOnly}` checks only those layers plus their
ancestors, descendants and siblings (`src/mcp/incremental.rs`), with `covered` saying what ran.
Tests: `tests/incremental.rs`, unit tests next to the code, the attribution check in
`tests/mcp.rs::parallel_sessions_get_their_own_runs`.
### 2026-10-21: Design libraries and offline CLI
* Variables are now collections with modes, slash groups, four types and aliases, stored in `design.json` (project version 2; 0.4.0 refuses migrated projects). Each page keeps a derived cache; every edit is one undo step per page, and the page-only removals are gone. Per-page active mode per collection.
+59 -18
View File
@@ -12,9 +12,17 @@
</p>
<p align="center">
<img src="docs/screenshots/readme/hero.png" alt="PaperCraft with the Redesign page of its own design file open: layers on the left, the canvas in the middle, the inspector on the right" width="100%">
<a href="docs/demo.mp4">
<img src="docs/demo.gif" alt="PaperCraft's editor breaking apart into its panels on a 3D stage, then chat mode with an agent, annotations pinned to a sign-in screen, the variables and libraries modals, and the home screen" width="100%">
</a>
<br>
<sub>PaperCraft editing its own design file (the Redesign page).</sub>
<sub>A 40-second tour: the editor, chat mode, annotations and design systems. <a href="docs/demo.mp4">Watch it in full quality</a> (MP4, 1080p60). Rendered from the app's own design with <a href="tools/showcase/README.md">tools/showcase</a>, not screen-recorded.</sub>
</p>
<p align="center">
<img src="docs/screenshots/readme/hero.png" alt="PaperCraft with the Pulse habit-tracker design open: layers on the left, three annotated screens on the canvas, the inspector on the right" width="100%">
<br>
<sub>The finished design: three screens, a component library and annotations.</sub>
</p>
<p align="center">
@@ -25,6 +33,7 @@
<a href="#keyboard-shortcuts">Shortcuts</a> ·
<a href="#status-and-known-limits">Status</a> ·
<a href="#project-layout">Layout</a> ·
<a href="#contributing">Contributing</a> ·
<a href="#license-and-credits">License</a>
</p>
@@ -54,11 +63,11 @@ It is an alpha. It is good for building screens, design systems and sketches, an
<table>
<tr>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/variables.png" alt="The Variables modal listing the Colors collection with four Catppuccin modes" width="100%">
<br><sub>Variables modal: collections, modes, groups and types.</sub>
<img src="docs/screenshots/readme/variables.png" alt="The Variables modal listing the Colors collection with Light and Dark values for brand, foreground and background" width="100%">
<br><sub>Variables modal: collections, modes (Light and Dark here), groups and types.</sub>
</td>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/assets.png" alt="The Assets side view with components grouped into Banner, Button, Checkbox and Chip" width="100%">
<img src="docs/screenshots/readme/assets.png" alt="The Assets side view with the Habit row component and the shadcn Button kit" width="100%">
<br><sub>Assets view: components grouped by name, from this file and enabled libraries.</sub>
</td>
</tr>
@@ -68,12 +77,32 @@ It is an alpha. It is good for building screens, design systems and sketches, an
<br><sub>Libraries modal: publish this file, enable libraries from the store.</sub>
</td>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/agent-review.png" alt="Agent run review: a list of changes on the left and green, yellow and red outlines on the canvas" width="100%">
<br><sub>Review of an agent run, with the changes outlined on the canvas.</sub>
<img src="docs/screenshots/readme/agent-review.png" alt="The Agent side view: a finished run with Review and Revert, above the list of earlier runs" width="100%">
<br><sub>The Agent view: runs, with Review and Revert for the latest.</sub>
</td>
</tr>
</table>
<table>
<tr>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/toolbar-panels.png" alt="The toolbar with the menu button folded into a back arrow, the File to Theme strip, and the floating left panel with a slim grip beside it" width="100%">
<br><sub>Toolbar and panels: the menu button shows or hides the File / Edit strip, and the slim grip between a panel and the canvas resizes it (double click resets). <kbd>/</kbd> hides both side panels.</sub>
</td>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/menus.png" alt="The Tools menu open below its button, each tool with an icon and its shortcut" width="100%">
<br><sub>Menus open a little below their button, are never narrower than 180 px, and carry an icon next to each item.</sub>
</td>
</tr>
<tr>
<td width="50%" valign="top">
<img src="docs/screenshots/readme/annotations.png" alt="The Annotations side view listing five notes grouped by screen, with their category chips" width="100%">
<br><sub>Annotations: rule, behaviour, content and data notes, kept with the layers they describe.</sub>
</td>
<td width="50%" valign="top"></td>
</tr>
</table>
**Prototyping and review**
* Prototype links between frames and a **Present** mode that follows them with a camera glide.
* **Version history:** named and automatic saved versions per page; rename, restore, delete.
@@ -90,11 +119,11 @@ Projects live in `~/Documents/papercraft/<name>/` (`project.json`, one JSON file
## Agents
PaperCraft exposes **70 tools** (`src/mcp/tools.rs`) three ways. They run the same code inside the app.
PaperCraft exposes **98 tools** (`src/mcp/tools.rs`) three ways. They run the same code inside the app.
| Way | Notes |
|---|---|
| MCP over HTTP | Built into the app: `http://127.0.0.1:29980/mcp`. Loopback only; browser requests (with an `Origin` header) and foreign `Host` headers are rejected. `PAPERCRAFT_MCP_PORT` changes the port, `PAPERCRAFT_MCP=0` turns it off. |
| MCP over HTTP | Built into the app: `http://127.0.0.1:29980/mcp`. Loopback only; browser requests (with an `Origin` header) and foreign `Host` headers are rejected. `?tools=core` lists 33 common tools in full plus `search_tools` and `call_tool`, which reach the rest (about a third of the schema size); lean chat agents use it. `PAPERCRAFT_MCP_PORT` changes the port, `PAPERCRAFT_MCP=0` turns it off. |
| MCP over stdio | `papercraft-mcp` relays stdio to the running app. It lists tools even when the app is closed. |
| Shell | `papercraft-cli` calls the same tools, so an agent with a terminal needs no schemas in its context. |
@@ -138,10 +167,10 @@ EOF
### Chat
Chat mode (⌘J, or `Design | Chat` in the top bar) puts a conversation next to the canvas. It runs Claude Code and Codex through their CLIs, any agent that speaks the Agent Client Protocol (ACP, such as OpenCode, Gemini CLI or Cursor) found on your PATH, or the Anthropic API with your own key. It shows tool calls, checkpoints and run review in the thread. Chats are saved in the project's `chats/` folder. See [docs/chat.md](docs/chat.md).
Chat mode (⌘J, or `Design | Chat` in the top bar) puts a conversation next to the canvas. It runs Claude Code and Codex through their CLIs, any agent that speaks the Agent Client Protocol (ACP, such as OpenCode, Cursor or Goose) found on your PATH, or the Anthropic API with your own key. It shows tool calls, checkpoints and run review in the thread. Chats are saved in the project's `chats/` folder. See [docs/chat.md](docs/chat.md).
<p align="center">
<img src="docs/screenshots/readme/chat.png" alt="Chat mode: the chat column on the left shows a finished agent run with its Review, Revert and Accept actions, and the canvas on the right" width="100%">
<img src="docs/screenshots/readme/chat.png" alt="Chat mode: the chat column on the left with its starter prompts and input, and the annotated habit-tracker screens on the canvas" width="100%">
<br>
<sub>Chat mode with a finished agent run: review, revert or accept from the thread.</sub>
</p>
@@ -162,12 +191,13 @@ Releases are at [git.marcospaulo.dev.br/masi/papercraft/releases](https://git.ma
| Build | File |
|---|---|
| Windows (x86_64) | `papercraft-<version>-windows-x86_64.zip`: `papercraft.exe`, `papercraft-cli.exe`, `papercraft-mcp.exe` and `papercraft-relay.exe` |
| macOS app (Apple silicon) | `papercraft-<version>-macos-aarch64.dmg`, containing PaperCraft.app |
| Linux (x86_64) | `papercraft-<version>-linux-x86_64.tar.gz` |
| CLI tools, macOS | `papercraft-cli-<version>-macos-aarch64.zip` (`papercraft-cli` and `papercraft-mcp`) |
| CLI tools, Linux | `papercraft-cli-<version>-linux-x86_64.tar.gz` |
macOS builds are ad-hoc signed and not notarized: on another Mac, open the app with right-click, Open the first time. Other platforms: build from source.
The Windows zip is unsigned: SmartScreen may ask you to confirm the first run. macOS builds are ad-hoc signed and not notarized: on another Mac, open the app with right-click, Open the first time. Other platforms: build from source.
### Build from source
@@ -179,9 +209,10 @@ cargo build --release
./target/release/papercraft ~/Documents/papercraft/Plan # open a project folder
./target/release/papercraft --demo # sample board, not saved
packaging/macos/package.sh # PaperCraft.app on a DMG, plus the CLI zip
pwsh packaging/windows/package.ps1 -Arch x64 # Windows MSI, portable zip and CLI zip
```
**macOS:** `xcode-select --install` for the linker. It renders with Metal through wgpu. **Linux:** install the runtime libraries (`sudo apt install libxkbcommon-x11-0 libxcb-xkb1 libvulkan1 mesa-vulkan-drivers` on Debian or Ubuntu); it runs on X11 and Wayland through Vulkan, with GL as a fallback, and file dialogs use the XDG desktop portal.
**Windows:** the MSVC build tools (Visual Studio Build Tools with the C++ workload); it renders through wgpu on Direct3D 12 or Vulkan. **macOS:** `xcode-select --install` for the linker. It renders with Metal through wgpu. **Linux:** install the runtime libraries (`sudo apt install libxkbcommon-x11-0 libxcb-xkb1 libvulkan1 mesa-vulkan-drivers` on Debian or Ubuntu); it runs on X11 and Wayland through Vulkan, with GL as a fallback, and file dialogs use the XDG desktop portal.
To run a test copy next to your own PaperCraft, give it its own folder and port: `PAPERCRAFT_HOME=/tmp/pc PAPERCRAFT_MCP_PORT=29998 ./target/release/papercraft`.
@@ -228,24 +259,32 @@ The scenario tests drive the editor with synthetic pointer and keyboard events;
| PageUp / PageDown | Previous / next page | ⌘\\ | Show / hide UI |
| ⌘J | Design / Chat mode | ⌘L | Add selection to chat (Chat mode) |
| N | Annotate tool | ⌘⇧A | Annotations: full, badges, hidden |
| / | Show / hide the side panels | | |
Draw a frame around existing shapes and it adopts them; dragging a layer into or out of a frame re-parents it. Hold ⌘ while dragging to suspend snapping.
## Status and known limits
> [!IMPORTANT]
> PaperCraft is an alpha. [ROADMAP.md](ROADMAP.md) lists what is missing, in priority order; [PROGRESS.md](PROGRESS.md) is the milestone history.
> PaperCraft is an alpha (0.8.x). It is used daily on Windows, macOS and Linux, and its project format migrates forward, but expect rough edges. [ROADMAP.md](ROADMAP.md) lists what is missing, in priority order; [PROGRESS.md](PROGRESS.md) is the milestone history.
**What works today:** local projects with pages and an infinite canvas; vector, text, auto layout, constraints, components with properties and variants, variables with modes, shared libraries and screens; annotations, lint and Present mode with prototype links; PNG, SVG and PDF export, Figma, Excalidraw and SVG import; the MCP server, CLI (online and offline) and in-app Chat mode; versions, agent runs with review and revert; optional relay sync.
**Known limits:**
* **No rotation.** Nodes are axis-aligned boxes; rotated imports are baked into paths.
* **Text** is Inter only, with no shaping (no ligatures, complex scripts or RTL) and no font picker. IME preedit is not displayed.
* **Rendering approximations.** Frame clipping is a rectangle, so rounded frames do not clip their corners. Overlay, dodge, burn, light, difference, exclusion and the HSL blend modes draw as Normal on the canvas (they are exact in SVG and PDF export). There is no layer or background blur yet.
* **Variables:** gradients keep fixed colors; there are no text styles and no component variants or properties.
* **Variables:** gradients keep fixed colors, and there are no text styles (named type styles that update every text layer).
* **Components:** properties (text, bool, icon, enum) and `merge_variants` cover variants, but there are no variant sets with several axes, and no responsive per-breakpoint variants or breakpoint preview.
* **Prototyping:** links and flows work in Present mode; smart transitions and hover states do not exist yet.
* **Booleans are destructive:** the result is a new vector path with flattened curves.
* **`write_html`** ignores margin, tables and transforms (it reports them as `ignoredStyles`) and does not fetch remote images; pass a local path or a `data:` URI.
* **Agents:** one project is open at a time in the app, and `move_nodes` stays on one page. The in-app Chat mode runs Claude Code, Codex, ACP agents or your own Anthropic key. Agents that are not in Chat connect over MCP or the CLI.
* **Sharing** has no live presence (cursors, selections) yet.
* **Scale:** cached frames take about 3 ms at 10k visible nodes; it is not tuned for 100k.
* **Pen pressure** is simulated from stroke velocity on macOS tablets, because winit 0.30 does not expose it for mouse-like pen events.
* **Releases** cover macOS (Apple silicon) and Linux x86_64. Windows packaging scripts exist but are not part of releases.
* **Releases:** the Gitea releases ship Windows x86_64. macOS (Apple silicon) and Linux x86_64 builds come from the packaging scripts or from source; the Linux build is not part of the current releases.
* **PDF** text is outlined, not selectable.
## Project layout
@@ -280,12 +319,14 @@ analysis/ behaviour studies and design notes
docs/ guides and screenshots
```
## Contributing
Bug reports, ideas, docs and code are welcome. Start with [CONTRIBUTING.md](CONTRIBUTING.md) (setup, checks, commits, pull requests) and [RULES.md](RULES.md) (the standards a change is reviewed against). AI tools are welcome under the [AI usage policy](AI_POLICY.md), and coding agents should read [AGENTS.md](AGENTS.md). Security problems go through [SECURITY.md](SECURITY.md), and everyone follows the [Code of Conduct](CODE_OF_CONDUCT.md).
## License and credits
Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option.
Bundled fonts, icons and other assets keep their own licenses, listed in [ATTRIBUTION.md](ATTRIBUTION.md); Inter is © The Inter Project Authors under the SIL Open Font License 1.1. Some algorithms are ported from MIT-licensed projects (rough.js, hachure-fill, Excalidraw), with their notices in [THIRD_PARTY.md](THIRD_PARTY.md).
PaperCraft started as a fork of [storytold/paper-rs](https://git.marcospaulo.dev.br/storytold/paper-rs); thanks to its contributors.
PaperCraft is not affiliated with Paper or Figma. Only the behavior of other tools was studied (see `analysis/`); no code, shaders, icons, fonts or assets from them are used.
+5 -4
View File
@@ -22,7 +22,7 @@ Agents can already build a design system through the tools; people cannot yet ma
| 7 | Number variables (spacing, radius, font size) | **done** | Typed number, string and boolean variables in collections with groups and aliases; fields bind (size, gap, padding, radius, font size, line height, opacity, visible) in the inspector and `bind_variable`. |
| 8 | Stroke and gradient variables | partial | Strokes and text color bind to variables. Gradients keep fixed colors. |
| 9 | Text styles | todo | Named type styles (Heading / L, Body / M) that update every text layer using them. |
| 10 | Component variants and properties | todo | Variants are separate components named `Button / Primary`. No variant sets, boolean or text properties. |
| 10 | Component variants and properties | partial | Main components carry text, bool, icon and enum properties; `merge_variants` folds per-state components into one. Multi-axis variant sets todo. |
| 11 | Swap instance, detach by tool | **done** | `swap_instance` and `detach_instance` MCP tools + UI controls in Design tab and Inspector. |
| 12 | Cross-page and shared libraries | **done** | Instances of a main on any page; publish a project as a library, enable, update and disable it in other projects (Libraries modal, `*_library` tools). |
| 13 | Export design tokens | **done** | `get_variables` with `format: "css"`, `"json"` (W3C DTCG + modes), or `"tailwind"` (`@theme`), plus Copy CSS / Copy JSON in Design tab. |
@@ -32,7 +32,7 @@ Agents can already build a design system through the tools; people cannot yet ma
| # | Item | Status | Notes |
|---|---|---|---|
| 15 | MCP and CLI tool surface | **done** | 67 tools, one undo step per call, `papercraft-cli list / schema / call`. |
| 15 | MCP and CLI tool surface | **done** | 98 tools, one undo step per call, `papercraft-cli list / schema / call`. |
| 16 | Agent skills | **done** | `skills/papercraft` and `skills/papercraft-design`. |
| 17 | Accessibility annotations | todo | Reading order, roles and alt text on layers, checked by the linter. |
| 18 | Code export from frames | **done** | `get_jsx` supports `format: "inline"`, `"tailwind"` (Tailwind utility classes), and `"html"` (HTML + CSS rules with `:root` variables). |
@@ -63,7 +63,7 @@ Agents can already build a design system through the tools; people cannot yet ma
| 32 | Version history | **done** | Named and automatic saved versions per page: rename, restore, delete (`src/versions.rs`). Restore now includes variables and tokens; compare todo. |
| 33 | Figma import | **done** | `.fig` binary (Kiwi wire decoder + zip container) and Figma REST API JSON import in `src/io/fig.rs`. |
| 34 | Multiplayer and cloud sync | partial | Optional shared projects through the relay, with merge of concurrent edits to one page. Presence (cursors, selections) todo. Stays optional. |
| 35 | Release builds for Windows and Linux | todo | The code targets them (wgpu, winit); packaging today is the macOS DMG and the `papercraft-cli` zip. |
| 35 | Release builds for Windows and Linux | partial | Windows x86_64 zips are released and `packaging/windows` builds an MSI; Linux x86_64 is not in the current releases. |
## 5. Quality
@@ -92,4 +92,5 @@ Spec: [specs/agent-runs/spec.md](specs/agent-runs/spec.md), tasks: [specs/agent-
| # | Item | Status | Notes |
|---|---|---|---|
| 47 | Design annotations | **done** | Notes with leader lines and categories, templates per project and page, view modes (⌘⇧A), Annotate tool (N), side view, inspector section, six MCP tools; see `docs/annotations.md`. |
| 48 | ACP agents in Chat | **done** | Any ACP agent on the PATH (OpenCode works; Gemini CLI refuses ACP clients for now; Cursor needs `cursor-agent login`), npx agents after enabling, custom agents; see `docs/chat.md`. |
| 48 | ACP agents in Chat | **done** | Any ACP agent on the PATH (OpenCode works; Cursor needs `cursor-agent login`), npx agents after enabling, custom agents; see `docs/chat.md`. |
| 48b | Free agents | **done** | OpenCode and Pi on free models with `Free (auto)` rotation, Pi provider keys, an install dialog per OS; see `specs/free-agents`. |
+124
View File
@@ -0,0 +1,124 @@
# Project rules
The standards a change to PaperCraft is reviewed against. [CONTRIBUTING.md](CONTRIBUTING.md) says
*how* to contribute; this file says what "good" means once you do.
## Code
* **Immutability by default.** Return new values rather than mutating shared ones. The document
changes only through `Editor::edit` transactions, so every change is one undo step and is
logged for the renderer's cache.
* **Small, cohesive files.** 200 to 400 lines is typical, 800 is the ceiling. Split a module
when it grows past that. Organise by feature, not by type.
* **Small functions** (under about 50 lines) and **shallow nesting** (4 levels at most); prefer
early returns.
* **Handle errors explicitly.** No `unwrap` or `expect` on user data, file contents or network
responses; return an error with context. User-facing messages are plain language. Never swallow
an error silently.
* **Validate at the boundaries**: MCP and CLI arguments, imported files (Figma, Excalidraw, SVG),
project folders and anything from the relay. Treat all of it as untrusted.
* **No magic numbers.** Name thresholds, sizes and delays as constants; UI measures come from
the theme, not literals.
* **No dead code, debug prints or commented-out blocks.**
* **Match the surrounding code**: naming, comment density and idiom. Comments say *why*, not
*what*.
* **Cross-platform.** Code must build on Windows, macOS and Linux and for the web build. Gate
platform code with `cfg`, never assume a path separator, and keep
`cargo check --lib --target wasm32-unknown-unknown` passing.
* **No new heavy dependencies** without discussion. Licenses must be compatible with
MIT OR Apache-2.0.
## Data safety
PaperCraft stores people's work. Treat that as the top priority.
* Writes to projects are **atomic** (`src/io/atomic.rs`); never write a page in place.
* A format change bumps the project format version and ships a **migration**. Older data must
keep loading; if a newer build writes something an older build cannot read, the older build
must refuse it rather than corrupt it.
* Agent and sync paths must never silently drop a layer. If a merge cannot keep something, it
says so.
* Destructive operations are undoable or confirmed.
## The machine you build on
A full build links dozens of test binaries and can take every core and most of the memory of a
desktop. The person is still using that machine, often with PaperCraft open.
* **Look before you start a heavy job.** Check free memory, CPU load and what is already running
(the person's app, another build, other agents) before a build, a test run or clippy. If memory
is short or another build is running, wait or ask.
* **Leave room.** Cap Cargo at no more than half the logical cores (`-j 8` on a 16-core machine,
or `CARGO_BUILD_JOBS`) and the test harness too (`-- --test-threads=8`); go lower when memory is
tight, since linking is the peak. Run heavy jobs at low priority (`nice` on Unix, Below Normal
on Windows).
* **One heavy job at a time.** Never run two builds side by side; each target folder is a full
build's worth of compiling and linking. Parallel agents share one build slot.
* **Run only what your change touches**: `cargo check` before `cargo build`, and the test
targets that cover the change (`cargo test --test incremental`). The full suite runs on GitHub
before a release (`.github/workflows/test.yml`), not on a desktop.
* **Stop at once when the person says the machine is slow**, then check that no compiler, linker
or test process of yours is left over.
* **Clean up.** Delete scratch target folders and worktrees when the work is merged; they grow to
tens of gigabytes.
## UI
* **Use the shared kit.** Controls, spacing, radii and colours come from `craft-ui`,
`src/ui/kit.rs` and the theme, not one-off values.
* **Themes are modes.** Never branch on a theme to pick a colour; read it from the palette.
* **Nothing overflows.** Text truncates with an ellipsis, rows keep their controls inside, and
panels step aside when the window is narrow. Test a narrow window and a long name.
* **Targets are at least 44 px** where a person taps or clicks something primary, and everything
stays reachable from the keyboard.
* **Animation is quick**: eased and under about 200 ms.
* **Look at it.** Run the app or take a headless screenshot at scale 2 in each theme before
asking for review.
## MCP tools and the CLI
* Every tool has a precise one-paragraph description, a JSON schema, and a test in
`tests/mcp.rs`. Descriptions are what agents read; write them for that reader.
* One tool call is **one undo step**.
* Names follow the existing style (`verb_noun`); reuse Paper-compatible names and argument shapes
where a tool has an equivalent.
* Reads are cheap and bounded. Apply limits, say when you truncated, and never dump the whole
document by default.
* The server listens on `127.0.0.1` only and rejects browser origins. Do not weaken that.
* Update the tool count in the README and ROADMAP when you add or remove tools.
## Designs made with PaperCraft
These apply to example projects, templates, screenshots and skill content, and are what the
skills teach agents.
* **Themes are modes, never copies.** One artboard per screen. Give each variable a value per
mode and switch with `set_variable_mode`. Do not duplicate artboards as light and dark
versions, and do not rebuild a screen with other hex values.
* **Every colour on a themed screen is a variable** (`var(--name)` on fills and text,
`bind_variable` for strokes, opacity and visibility).
* **No overflow.** Nothing extends past its artboard or clipping parent. Scrolling content is
clipped inside its viewport and ends above floating bars with a fade and enough bottom padding.
Check each artboard programmatically *and* by screenshot in each mode.
* Run `lint_design` and fix what it finds: contrast, the 4 px grid, type scale and 44 px targets.
* Release design locks when you are done so the next person can inspect.
## Skills and docs
* Skills in [skills/](skills) are instructions for agents: keep them short, concrete and tested
against a real agent run.
* Docs describe what exists. Do not document a feature that is not built; put plans in `specs/`
and `ROADMAP.md`.
* Screenshots come from the real app, in a generic project (no personal data, local paths or
real names), at scale 2.
## Branding
The tree carries no third-party product branding; `.debrand` lists the forbidden patterns and
the check runs in review. Describe PaperCraft on its own terms.
## Review
A pull request is approved when it has no unresolved **critical** (security, data loss) or
**high** (bug, significant quality) issues. Medium and low comments are suggestions the author
may take or decline with a reason.
+29
View File
@@ -0,0 +1,29 @@
# Security policy
## Supported versions
PaperCraft is an alpha. Security fixes go into the latest release and `main`.
## Reporting a vulnerability
**Do not open a public issue.** Use GitHub's private reporting: the repository's **Security** tab,
then **Report a vulnerability**. Include what you found, how to reproduce it, the version and
platform, and the impact you see.
A maintainer will acknowledge the report as soon as they can and keep you updated until it is
fixed. Please give us reasonable time to ship a fix before you disclose it.
## What counts
Examples of in-scope problems:
* the MCP server (`127.0.0.1:29980`) or the relay accepting requests it should reject (browser
origins, DNS rebinding, another user);
* path traversal or code execution from a project folder, a design-system or library package, or
an imported Figma, Excalidraw or SVG file;
* the updater or the agent integrations running something the person did not approve;
* secrets (an Anthropic key, relay tokens) written to disk unprotected or sent anywhere
unexpected.
PaperCraft is local-first: it opens no network port beyond the loopback MCP server unless you
share a project through a relay you run.
+12
View File
@@ -62,3 +62,15 @@ SOFTWARE.
Inter 4.1 by Rasmus Andersson — SIL Open Font License 1.1 (`assets/fonts/OFL.txt`). Regular,
Medium, SemiBold, Bold, Italic and Bold Italic are bundled unmodified.
Interface fonts, SIL Open Font License 1.1, Latin subsets as served by Google Fonts, unmodified
(Regular, Medium, SemiBold): Geist (Vercel), Figtree (Erik Kennedy), DM Sans (Colophon Foundry,
Jonny Pinhorn), Nunito (Vernon Adams, Jacques Le Bailly), Quicksand (Andrew Paglinawan),
Fredoka (Milena Brandão, Hafontia), and Varela Round (Joe Prince; Regular only).
## Agent logos
`assets/agents/*.svg` are from LobeHub Icons (`@lobehub/icons-static-svg` 1.80.0,
https://github.com/lobehub/lobe-icons), MIT License, Copyright (c) LobeHub. They are used
unmodified to identify each coding agent in the agent picker. The marks are trademarks of
their owners.
+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cline</title><path d="M17.035 3.991c2.75 0 4.98 2.24 4.98 5.003v1.667l1.45 2.896a1.01 1.01 0 01-.002.909l-1.448 2.864v1.668c0 2.762-2.23 5.002-4.98 5.002H7.074c-2.751 0-4.98-2.24-4.98-5.002V17.33l-1.48-2.855a1.01 1.01 0 01-.003-.927l1.482-2.887V8.994c0-2.763 2.23-5.003 4.98-5.003h9.962zM8.265 9.6a2.274 2.274 0 00-2.274 2.274v4.042a2.274 2.274 0 004.547 0v-4.042A2.274 2.274 0 008.265 9.6zm7.326 0a2.274 2.274 0 00-2.274 2.274v4.042a2.274 2.274 0 104.548 0v-4.042A2.274 2.274 0 0015.59 9.6z"></path><path d="M12.054 5.558a2.779 2.779 0 100-5.558 2.779 2.779 0 000 5.558z"></path></svg>

After

Width:  |  Height:  |  Size: 750 B

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Cursor</title><path d="M22.106 5.68L12.5.135a.998.998 0 00-.998 0L1.893 5.68a.84.84 0 00-.419.726v11.186c0 .3.16.577.42.727l9.607 5.547a.999.999 0 00.998 0l9.608-5.547a.84.84 0 00.42-.727V6.407a.84.84 0 00-.42-.726zm-.603 1.176L12.228 22.92c-.063.108-.228.064-.228-.061V12.34a.59.59 0 00-.295-.51l-9.11-5.26c-.107-.062-.063-.228.062-.228h18.55c.264 0 .428.286.296.514z"></path></svg>

After

Width:  |  Height:  |  Size: 547 B

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>GithubCopilot</title><path d="M19.245 5.364c1.322 1.36 1.877 3.216 2.11 5.817.622 0 1.2.135 1.592.654l.73.964c.21.278.323.61.323.955v2.62c0 .339-.173.669-.453.868C20.239 19.602 16.157 21.5 12 21.5c-4.6 0-9.205-2.583-11.547-4.258-.28-.2-.452-.53-.453-.868v-2.62c0-.345.113-.679.321-.956l.73-.963c.392-.517.974-.654 1.593-.654l.029-.297c.25-2.446.81-4.213 2.082-5.52 2.461-2.54 5.71-2.851 7.146-2.864h.198c1.436.013 4.685.323 7.146 2.864zm-7.244 4.328c-.284 0-.613.016-.962.05-.123.447-.305.85-.57 1.108-1.05 1.023-2.316 1.18-2.994 1.18-.638 0-1.306-.13-1.851-.464-.516.165-1.012.403-1.044.996a65.882 65.882 0 00-.063 2.884l-.002.48c-.002.563-.005 1.126-.013 1.69.002.326.204.63.51.765 2.482 1.102 4.83 1.657 6.99 1.657 2.156 0 4.504-.555 6.985-1.657a.854.854 0 00.51-.766c.03-1.682.006-3.372-.076-5.053-.031-.596-.528-.83-1.046-.996-.546.333-1.212.464-1.85.464-.677 0-1.942-.157-2.993-1.18-.266-.258-.447-.661-.57-1.108-.32-.032-.64-.049-.96-.05zm-2.525 4.013c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zm5 0c.539 0 .976.426.976.95v1.753c0 .525-.437.95-.976.95a.964.964 0 01-.976-.95v-1.752c0-.525.437-.951.976-.951zM7.635 5.087c-1.05.102-1.935.438-2.385.906-.975 1.037-.765 3.668-.21 4.224.405.394 1.17.657 1.995.657h.09c.649-.013 1.785-.176 2.73-1.11.435-.41.705-1.433.675-2.47-.03-.834-.27-1.52-.63-1.813-.39-.336-1.275-.482-2.265-.394zm6.465.394c-.36.292-.6.98-.63 1.813-.03 1.037.24 2.06.675 2.47.968.957 2.136 1.104 2.776 1.11h.044c.825 0 1.59-.263 1.995-.657.555-.556.765-3.187-.21-4.224-.45-.468-1.335-.804-2.385-.906-.99-.088-1.875.058-2.265.394zM12 7.615c-.24 0-.525.015-.84.044.03.16.045.336.06.526l-.001.159a2.94 2.94 0 01-.014.25c.225-.022.425-.027.612-.028h.366c.187 0 .387.006.612.028-.015-.146-.015-.277-.015-.409.015-.19.03-.365.06-.526a9.29 9.29 0 00-.84-.044z"></path></svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Goose</title><path d="M21.595 23.61c1.167-.254 2.405-.944 2.405-.944l-2.167-1.784a12.124 12.124 0 01-2.695-3.131 12.127 12.127 0 00-3.97-4.049l-.794-.462a1.115 1.115 0 01-.488-.815.844.844 0 01.154-.575c.413-.582 2.548-3.115 2.94-3.44.503-.416 1.065-.762 1.586-1.159.074-.056.148-.112.221-.17.003-.002.007-.004.009-.007.167-.131.325-.272.45-.438.453-.524.563-.988.59-1.193-.061-.197-.244-.639-.753-1.148.319.02.705.272 1.056.569.235-.376.481-.773.727-1.171.165-.266-.08-.465-.086-.471h-.001V3.22c-.007-.007-.206-.25-.471-.086-.567.35-1.134.702-1.639 1.021 0 0-.597-.012-1.305.599a2.464 2.464 0 00-.438.45l-.007.009c-.058.072-.114.147-.17.221-.397.521-.743 1.083-1.16 1.587-.323.391-2.857 2.526-3.44 2.94a.842.842 0 01-.574.153 1.115 1.115 0 01-.815-.488l-.462-.794a12.123 12.123 0 00-4.049-3.97 12.133 12.133 0 01-3.13-2.695L1.332 0S.643 1.238.39 2.405c.352.428 1.27 1.49 2.34 2.302C1.58 4.167.73 3.75.06 3.4c-.103.765-.063 1.92.043 2.816.726.317 1.961.806 3.219 1.066-1.006.236-2.11.278-2.961.262.15.554.358 1.119.64 1.688.119.263.25.52.39.77.452.125 2.222.383 3.164.171l-2.51.897a27.776 27.776 0 002.544 2.726c2.031-1.092 2.494-1.241 4.018-2.238-2.467 2.008-3.108 2.828-3.8 3.67l-.483.678c-.25.351-.469.725-.65 1.117-.61 1.31-1.47 4.1-1.47 4.1-.154.486.202.842.674.674 0 0 2.79-.861 4.1-1.47.392-.182.766-.4 1.118-.65l.677-.483c.227-.187.453-.37.701-.586 0 0 1.705 2.02 3.458 3.349l.896-2.511c-.211.942.046 2.712.17 3.163.252.142.509.272.772.392.569.28 1.134.49 1.688.64-.016-.853.026-1.956.261-2.962.26 1.258.75 2.493 1.067 3.219.895.106 2.051.146 2.816.043a73.87 73.87 0 01-1.308-2.67c.811 1.07 1.874 1.988 2.302 2.34h-.001z"></path></svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Kimi</title><path d="M21.846 0a1.923 1.923 0 110 3.846H20.15a.226.226 0 01-.227-.226V1.923C19.923.861 20.784 0 21.846 0z"></path><path d="M11.065 11.199l7.257-7.2c.137-.136.06-.41-.116-.41H14.3a.164.164 0 00-.117.051l-7.82 7.756c-.122.12-.302.013-.302-.179V3.82c0-.127-.083-.23-.185-.23H3.186c-.103 0-.186.103-.186.23V19.77c0 .128.083.23.186.23h2.69c.103 0 .186-.102.186-.23v-3.25c0-.069.025-.135.069-.178l2.424-2.406a.158.158 0 01.205-.023l6.484 4.772a7.677 7.677 0 003.453 1.283c.108.012.2-.095.2-.23v-3.06c0-.117-.07-.212-.164-.227a5.028 5.028 0 01-2.027-.807l-5.613-4.064c-.117-.078-.132-.279-.028-.381z"></path></svg>

After

Width:  |  Height:  |  Size: 786 B

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Mistral</title><path clip-rule="evenodd" d="M3.428 3.4h3.429v3.428h3.429v3.429h-.002 3.431V6.828h3.427V3.4h3.43v13.714H24v3.429H13.714v-3.428h-3.428v-3.429h-3.43v3.428h3.43v3.429H0v-3.429h3.428V3.4zm10.286 13.715h3.428v-3.429h-3.427v3.429z"></path></svg>

After

Width:  |  Height:  |  Size: 418 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M9.205 8.658v-2.26c0-.19.072-.333.238-.428l4.543-2.616c.619-.357 1.356-.523 2.117-.523 2.854 0 4.662 2.212 4.662 4.566 0 .167 0 .357-.024.547l-4.71-2.759a.797.797 0 00-.856 0l-5.97 3.473zm10.609 8.8V12.06c0-.333-.143-.57-.429-.737l-5.97-3.473 1.95-1.118a.433.433 0 01.476 0l4.543 2.617c1.309.76 2.189 2.378 2.189 3.948 0 1.808-1.07 3.473-2.76 4.163zM7.802 12.703l-1.95-1.142c-.167-.095-.239-.238-.239-.428V5.899c0-2.545 1.95-4.472 4.591-4.472 1 0 1.927.333 2.712.928L8.23 5.067c-.285.166-.428.404-.428.737v6.898zM12 15.128l-2.795-1.57v-3.33L12 8.658l2.795 1.57v3.33L12 15.128zm1.796 7.23c-1 0-1.927-.332-2.712-.927l4.686-2.712c.285-.166.428-.404.428-.737v-6.898l1.974 1.142c.167.095.238.238.238.428v5.233c0 2.545-1.974 4.472-4.614 4.472zm-5.637-5.303l-4.544-2.617c-1.308-.761-2.188-2.378-2.188-3.948A4.482 4.482 0 014.21 6.327v5.423c0 .333.143.571.428.738l5.947 3.449-1.95 1.118a.432.432 0 01-.476 0zm-.262 3.9c-2.688 0-4.662-2.021-4.662-4.519 0-.19.024-.38.047-.57l4.686 2.71c.286.167.571.167.856 0l5.97-3.448v2.26c0 .19-.07.333-.237.428l-4.543 2.616c-.619.357-1.356.523-2.117.523zm5.899 2.83a5.947 5.947 0 005.827-4.756C22.287 18.339 24 15.84 24 13.296c0-1.665-.713-3.282-1.998-4.448.119-.5.19-.999.19-1.498 0-3.401-2.759-5.947-5.946-5.947-.642 0-1.26.095-1.88.31A5.962 5.962 0 0010.205 0a5.947 5.947 0 00-5.827 4.757C1.713 5.447 0 7.945 0 10.49c0 1.666.713 3.283 1.998 4.448-.119.5-.19 1-.19 1.499 0 3.401 2.759 5.946 5.946 5.946.642 0 1.26-.095 1.88-.309a5.96 5.96 0 004.162 1.713z"></path></svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Qwen</title><path d="M12.604 1.34c.393.69.784 1.382 1.174 2.075a.18.18 0 00.157.091h5.552c.174 0 .322.11.446.327l1.454 2.57c.19.337.24.478.024.837-.26.43-.513.864-.76 1.3l-.367.658c-.106.196-.223.28-.04.512l2.652 4.637c.172.301.111.494-.043.77-.437.785-.882 1.564-1.335 2.34-.159.272-.352.375-.68.37-.777-.016-1.552-.01-2.327.016a.099.099 0 00-.081.05 575.097 575.097 0 01-2.705 4.74c-.169.293-.38.363-.725.364-.997.003-2.002.004-3.017.002a.537.537 0 01-.465-.271l-1.335-2.323a.09.09 0 00-.083-.049H4.982c-.285.03-.553-.001-.805-.092l-1.603-2.77a.543.543 0 01-.002-.54l1.207-2.12a.198.198 0 000-.197 550.951 550.951 0 01-1.875-3.272l-.79-1.395c-.16-.31-.173-.496.095-.965.465-.813.927-1.625 1.387-2.436.132-.234.304-.334.584-.335a338.3 338.3 0 012.589-.001.124.124 0 00.107-.063l2.806-4.895a.488.488 0 01.422-.246c.524-.001 1.053 0 1.583-.006L11.704 1c.341-.003.724.032.9.34zm-3.432.403a.06.06 0 00-.052.03L6.254 6.788a.157.157 0 01-.135.078H3.253c-.056 0-.07.025-.041.074l5.81 10.156c.025.042.013.062-.034.063l-2.795.015a.218.218 0 00-.2.116l-1.32 2.31c-.044.078-.021.118.068.118l5.716.008c.046 0 .08.02.104.061l1.403 2.454c.046.081.092.082.139 0l5.006-8.76.783-1.382a.055.055 0 01.096 0l1.424 2.53a.122.122 0 00.107.062l2.763-.02a.04.04 0 00.035-.02.041.041 0 000-.04l-2.9-5.086a.108.108 0 010-.113l.293-.507 1.12-1.977c.024-.041.012-.062-.035-.062H9.2c-.059 0-.073-.026-.043-.077l1.434-2.505a.107.107 0 000-.114L9.225 1.774a.06.06 0 00-.053-.031zm6.29 8.02c.046 0 .058.02.034.06l-.832 1.465-2.613 4.585a.056.056 0 01-.05.029.058.058 0 01-.05-.029L8.498 9.841c-.02-.034-.01-.052.028-.054l.216-.012 6.722-.012z"></path></svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

+1
View File
@@ -0,0 +1 @@
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Tencent</title><path d="M9.976 1L24 9.8l-10.587.015L10.723 23H5.489L8.18 9.8H3.244L1 5.4h8.077L9.976 1z"></path></svg>

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+13 -1
View File
@@ -1,6 +1,6 @@
# Driving PaperCraft with agents
PaperCraft exposes one set of 70 tools three ways. They all run the same code inside the app, so an agent sees what you see and every call is one undo step.
PaperCraft exposes one set of 88 tools three ways. They all run the same code inside the app, so an agent sees what you see and every call is one undo step.
| Way | Use it when | Needs the app running |
|---|---|---|
@@ -108,6 +108,18 @@ write_html {
EOF
```
## Checking only what changed
On a big page a full `lint_design` after every edit repeats the same output, and a `get_node_info` call per layer is slow (about 53 s for 397 layers). Check the change, then check everything once.
* Every write tool result includes `rev`, the document revision after the call. `get_basic_info` reports the current `rev`. Reads never change it.
* `get_changes {since, page?}` returns `{rev, since, truncated, changes: [{page, ids}]}`: what the edits since `since` touched. `truncated: true` means the log no longer reaches that far back, so do a full pass.
* `lint_design {changedOnly: true}` lints since this client's last lint; `{since: rev}` lints since a revision you name. A scoped lint checks the touched layers plus their ancestors, descendants, siblings and every layer bound to a changed variable, and reports its coverage in `covered`, for example `{checked: 12, total: 397, since: 40, partial: true}`. Plain `lint_design` is a full pass (`partial: false`). The `overflow` rule flags a layer that extends past its parent or clipping artboard (scroll frames are excluded).
* `get_bounds {nodeIds?, pageId?, depth?}` returns `columns` `[id, parent, x, y, w, h, absX, absY, clip]` and one row per layer for a subtree in one call (at most 2000 rows; then `truncated` and `continueWith`, the nodeIds for the next call). Use it instead of looping `get_node_info`.
* Always finish with one full `lint_design` (no arguments) and a screenshot per artboard in each theme mode before reporting. A partial lint speeds up the loop; it does not replace that pass.
In the app, read-only calls no longer draw per-layer outlines or keep the "Working" pill lit for long. The agent workflow, with an example, is in [incremental-checks.md](../skills/papercraft/references/incremental-checks.md).
## Design-system tools
* Variables: `get_variables` and `set_variables` take collections, groups (`color/brand/primary`), types (`color`, `number`, `string`, `boolean`), `mode`, `alias`, `description` and `hidden`; the old `{"name":"#hex"}` call still works and lands in the first collection. `create_collection` makes a collection with modes, `delete_variables` removes by name (refuses aliased ones unless `force`), `bind_variable` binds or detaches a field of layers (`stroke`, `text-color`, `width`, `height`, `gap`, `padding`, `radius`, `font-size`, `line-height`, `opacity`, `visible`).
+5 -4
View File
@@ -18,15 +18,16 @@ Chat drives the same tools as an agent connected over MCP, so the edits, runs, u
The ACP agents PaperCraft knows about, and what was checked on the development Mac:
* **OpenCode** (`opencode acp`): works.
* **Gemini CLI** (`gemini --acp`): found and started, but its server currently refuses ACP clients, so turns fail with the agent's error until Google changes that.
* **OpenCode** (`opencode acp`): works. Its free models (`opencode/*-free`, Big Pickle) need no key.
* **Cursor** (`cursor-agent acp`): sign in first with `cursor-agent login`.
* **Pi** (`pi-acp`, an adapter run through `npx`): enable it once in the agent panel or Settings, then it works like the others.
* **Pi** (`pi-acp`, an adapter run through `npx`): enable it once in the agent panel or Settings. `pi-acp` does not pass MCP servers on to Pi, so a lean Pi gets its own folder (`PI_CODING_AGENT_DIR`) whose only package is `pi-mcp-adapter`, connected to PaperCraft. Pi runs on the providers you add a key for in Settings → Chat → Pi: OpenRouter (`:free` models, 50 requests a day without credits), Groq, Gemini, NVIDIA NIM and Cerebras. Keys live in the OS secret store and reach Pi only as environment variables.
* **Goose, Kimi, Devin, Junie, Mistral Vibe, Hermes, OpenHands, Stakpak, VT Code, Poolside, Droid**: started with the command their own docs give. They are not all verified here.
* **Claude Code and Codex ACP adapters** (`npx`): listed under Enable via npx. Claude Code and Codex themselves use the direct backends above.
Agents that run through `npx` are off until you enable them, because enabling runs code that npx fetches from npm.
**Free models.** For OpenCode and Pi the model list starts on free models (`Free only`) and offers `Free (auto)`, the default for new chats on them. It starts on the free model with the largest context. When a model fails with a rate limit, quota, overload or "gone" error before any tool ran, the turn moves to the next free one (at most two more) and the thread says so. A model that failed is skipped for 15 minutes. A failure after a tool ran is not retried, because the design may already have changed.
ACP permissions: PaperCraft's own tools run without asking, except the destructive ones listed under Approval below. Any other tool the agent asks to run (its own shell or file tools, for example) shows an approval card with Approve or Deny in the thread, and Deny is sent back to the agent.
**Custom agents** are your own ACP agents: a name, a command (a program name on the resolved `PATH`, or a full path) and arguments. The arguments are split on spaces, with no quoting. Custom agents appear under Custom in the agent panel and in Settings → Chat.
@@ -68,7 +69,7 @@ Settings → Chat has the same list, so an agent can also be hidden from the pan
### Setup card
A chat with no backend chosen shows one card, "Connect an agent". It lists the agents detected on this computer, up to four, with `Show all agents` for the rest. Below them are `Add an API endpoint` and `Add custom agent`. The card never lists install steps.
A chat with no backend chosen shows one card, "Connect an agent". It lists the agents detected on this computer, up to four, with `Show all agents` for the rest. Below them are `Add an API endpoint` and `Add custom agent`. When OpenCode or Pi is not found, its row has `Install…`. The dialog offers the install methods that work on this OS (Homebrew, Scoop, npm, Nix or the agent's install script; on Linux also the distribution's own). It shows the exact command and runs it only after you click Install, streaming the output. Methods that need `sudo` or an admin shell, and npm when Node is missing or too old, are shown with the reason and a command to copy instead.
### Sign-in
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 14 MiB

BIN
View File
Binary file not shown.
+2
View File
@@ -93,6 +93,8 @@ Fill-based sets (Phosphor, Material) follow variables; Lucide is stroke-based an
| `layer-name` | warning | default names (Frame, Rectangle, Group...) on layers with children |
| `hard-coded-color` | warning, info | a literal color that equals a variable (warning), or a literal used repeatedly when variables exist (info) |
| `overflow` | error | a layer extending past its artboard or clipping parent (by how many px, on which side), or text taller than its box. Scrolling frames and layers pinned in them are exempt. Never auto-fixed by `fix: true`: resize, move or let the parent grow. `get_bounds` returns every rect in one call |
Instances are skipped (their main component is linted). The rules are conventions, not law: pass `scale` to match your own type scale and ignore `info` items you disagree with. Lint does not judge taste; always look at `get_screenshot` too.
## Libraries
Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 209 KiB

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 416 KiB

+62
View File
@@ -0,0 +1,62 @@
#!/bin/sh
# Build the landing page into a folder ready for GitHub Pages:
# sh site/build.sh <out-dir> [<previous-build-dir>]
# Needs curl, jq and ffmpeg. GH_TOKEN, when set, authenticates the GitHub API call.
# RELEASE_JSON=<file> uses that file instead of asking the API. The demo video is re-encoded only
# when docs/demo.mp4 changed since <previous-build-dir>, so a release-only update stays cheap.
# <out-dir> must be new or empty.
set -eu
cd "$(dirname "$0")/.."
OUT="${1:?usage: sh site/build.sh <out-dir> [<previous-build-dir>]}"
PREV="${2:-}"
REPO="${GITHUB_REPOSITORY:-nxtime/papercraft}"
if [ -n "$(ls -A "$OUT" 2>/dev/null)" ]; then
echo "$OUT is not empty; pass a new folder." >&2
exit 1
fi
mkdir -p "$OUT/media/agents"
cp site/index.html site/site.css site/site.js "$OUT/"
cp assets/app-icon/papercraft.svg "$OUT/media/icon.svg"
for agent in claude openai cursor goose kimi mistral qwen cline githubcopilot nousresearch; do
cp "assets/agents/$agent.svg" "$OUT/media/agents/"
done
for shot in hero chat annotations variables; do
cp "docs/screenshots/readme/$shot.png" "$OUT/media/"
done
# GitHub Pages runs Jekyll otherwise, which is slower and has nothing to do here.
touch "$OUT/.nojekyll"
# The page reads this shape; site.js maps the API's own shape onto it the same way.
if [ -n "${RELEASE_JSON:-}" ]; then
cp "$RELEASE_JSON" "$OUT/release.json"
else
set -- -fsSL -H "Accept: application/vnd.github+json"
if [ -n "${GH_TOKEN:-}" ]; then
set -- "$@" -H "Authorization: Bearer $GH_TOKEN"
fi
curl "$@" "https://api.github.com/repos/$REPO/releases/latest" | jq '{
tag: .tag_name,
name: .name,
publishedAt: .published_at,
url: .html_url,
assets: [.assets[] | {name, size, url: .browser_download_url, digest}]
}' > "$OUT/release.json"
fi
jq -e '.tag and (.assets | length > 0)' "$OUT/release.json" > /dev/null
SRC_SUM="$(sha256sum docs/demo.mp4 | cut -d' ' -f1)"
if [ -n "$PREV" ] && [ -f "$PREV/media/demo.mp4" ] && [ "$(cat "$PREV/media/demo.source" 2>/dev/null)" = "$SRC_SUM" ]; then
cp "$PREV/media/demo.mp4" "$PREV/media/poster.jpg" "$OUT/media/"
else
# 720p30 without audio keeps the tour near 2 MB, against 26 MB for the 1080p60 source.
ffmpeg -loglevel error -y -i docs/demo.mp4 -vf "scale=1280:-2,fps=30" -c:v libx264 -crf 28 \
-preset slow -pix_fmt yuv420p -an -movflags +faststart "$OUT/media/demo.mp4"
ffmpeg -loglevel error -y -ss 1.5 -i docs/demo.mp4 -frames:v 1 -vf scale=1280:-2 -q:v 4 \
"$OUT/media/poster.jpg"
fi
echo "$SRC_SUM" > "$OUT/media/demo.source"
echo "Built $(jq -r .tag "$OUT/release.json") into $OUT"
+375
View File
@@ -0,0 +1,375 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>PaperCraft</title>
<meta name="claude:viewport-fit-sides" content="cover">
<meta name="description" content="A native, local-first design and whiteboard canvas that AI agents can drive over MCP and a CLI.">
<meta name="theme-color" content="#11111b">
<meta property="og:title" content="PaperCraft">
<meta property="og:description" content="A native canvas that AI agents can drive. Local-first, no account, open source.">
<meta property="og:image" content="media/poster.jpg">
<link rel="icon" href="media/icon.svg" type="image/svg+xml">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Geist:wght@400;500;600&family=Geist+Mono:wght@400;500&display=swap">
<link rel="stylesheet" href="site.css">
</head>
<body>
<div class="stage" aria-hidden="true"><div class="glow a"></div><div class="glow b"></div><div class="glow c"></div><div class="spot"></div></div>
<header class="nav" id="nav">
<div class="wrap">
<div class="nav-bar acrylic">
<a class="brand" href="#top"><img src="media/icon.svg" alt="">PaperCraft</a>
<nav class="nav-links" aria-label="Sections">
<a href="#live">Agents</a>
<a href="#features">Features</a>
<a href="#local">Local-first</a>
<a href="#download">Download</a>
<a href="https://github.com/nxtime/papercraft">GitHub</a>
</nav>
<a class="btn btn-primary btn-sm" href="#download">Download <span class="ver" data-version>latest</span></a>
<span class="progress" aria-hidden="true"></span>
</div>
</div>
</header>
<main id="top">
<section class="hero wrap">
<img class="hero-mark rise" src="media/icon.svg" alt="PaperCraft app icon" width="76" height="76">
<div class="eyebrow rise d1">Rust · egui · wgpu · MCP</div>
<h1 class="serif" id="headline">A native canvas that <em>AI agents</em> can drive.</h1>
<p class="lede rise d2">PaperCraft is an infinite-canvas design tool and whiteboard. It runs on your desktop with no account and no cloud, and it serves the Model Context Protocol so Claude Code, Codex and other agents can edit the open canvas, screenshot it and lint it, one undo step per call.</p>
<div class="cta rise d3">
<a class="btn btn-primary" id="hero-dl" href="#download">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m0 0-5-5m5 5 5-5M4 21h16"/></svg>
<span id="hero-dl-label">Download</span> <span class="ver" data-version>latest</span>
</a>
<a class="btn btn-ghost" href="https://github.com/nxtime/papercraft">
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 .5a11.5 11.5 0 0 0-3.64 22.41c.58.1.79-.25.79-.56v-2c-3.2.7-3.88-1.37-3.88-1.37-.52-1.33-1.28-1.69-1.28-1.69-1.04-.71.08-.7.08-.7 1.15.08 1.76 1.19 1.76 1.19 1.03 1.76 2.69 1.25 3.35.96.1-.75.4-1.25.73-1.54-2.55-.29-5.24-1.28-5.24-5.69 0-1.26.45-2.29 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.17 1.18a11 11 0 0 1 5.77 0c2.2-1.49 3.17-1.18 3.17-1.18.63 1.59.23 2.76.11 3.05.74.8 1.19 1.83 1.19 3.09 0 4.42-2.7 5.39-5.26 5.68.41.36.78 1.06.78 2.14v3.17c0 .31.21.67.8.56A11.5 11.5 0 0 0 12 .5Z"/></svg>
Source on GitHub
</a>
</div>
<div class="facts rise d3"><span>Local-first, no account</span><span>98 MCP tools</span><span>Windows · macOS · Linux</span><span>MIT or Apache-2.0</span></div>
</section>
<section class="showcase wrap" aria-label="Product tour">
<div class="window acrylic" id="window">
<div class="chrome" aria-hidden="true"><i></i><i></i><i></i><span>PaperCraft · Landing page</span></div>
<video src="media/demo.mp4" poster="media/poster.jpg" autoplay muted loop playsinline preload="metadata" aria-label="A 40-second tour of the editor, chat mode, annotations and design systems"></video>
</div>
<p class="caption">A 40-second tour, rendered from the app's own design rather than screen-recorded.</p>
</section>
<section class="marquee" aria-label="Agents that work with PaperCraft">
<p class="eyebrow wrap">Chat mode runs the agents you already use</p>
<div class="track">
<div class="belt">
<span class="chip acrylic"><i style="--logo:url(media/agents/claude.svg)"></i>Claude Code</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/openai.svg)"></i>Codex</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/cursor.svg)"></i>Cursor</span>
<span class="chip acrylic"><i class="none"></i>OpenCode</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/goose.svg)"></i>Goose</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/kimi.svg)"></i>Kimi</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/mistral.svg)"></i>Mistral Vibe</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/qwen.svg)"></i>Qwen Code</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/cline.svg)"></i>Cline</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/githubcopilot.svg)"></i>Copilot CLI</span>
<span class="chip acrylic"><i style="--logo:url(media/agents/nousresearch.svg)"></i>Hermes</span>
<span class="chip acrylic"><i class="none"></i>Any ACP agent</span>
</div>
</div>
</section>
<section class="block wrap" id="live">
<div class="section-head reveal">
<div class="eyebrow">For agents</div>
<h2 class="serif">Watch an agent <em>build a screen.</em></h2>
<p>Each tool call is one undo step. The agent writes HTML that becomes real layers, switches the theme by changing a variable mode, takes a screenshot and lints the result.</p>
</div>
<div class="live acrylic reveal" id="live-demo">
<div class="log">
<div class="log-head"><span class="dot" aria-hidden="true"></span>Agent run · Sign in<span class="undo" id="undo-count">0 undo steps</span></div>
<ol class="calls" id="calls">
<li class="call"><span class="st"></span><b>create_artboard</b><span class="ms">9 ms</span><span class="arg">{"name": "Sign in", "width": 360, "height": 480}</span></li>
<li class="call"><span class="st"></span><b>set_variables</b><span class="ms">4 ms</span><span class="arg">{"bg", "surface", "text", "accent"} with Dark and Light modes</span></li>
<li class="call"><span class="st"></span><b>write_html</b><span class="ms">18 ms</span><span class="arg">&lt;div style="display:flex; flex-direction:column; gap:12px"&gt; → 6 layers</span></li>
<li class="call"><span class="st"></span><b>update_styles</b><span class="ms">3 ms</span><span class="arg">{"#submit": {"background": "var(--accent)"}}</span></li>
<li class="call"><span class="st"></span><b>set_variable_mode</b><span class="ms">2 ms</span><span class="arg">{"collection": "Colors", "mode": "Light"}</span></li>
<li class="call"><span class="st"></span><b>get_screenshot</b><span class="ms">41 ms</span><span class="arg">sign-in@2x.png</span></li>
<li class="call"><span class="st"></span><b>lint_design</b><span class="ms">7 ms</span><span class="arg">contrast, 4px grid, type scale, touch targets</span></li>
</ol>
</div>
<div class="board-area" aria-hidden="true">
<span class="board-label">Sign in</span>
<span class="mode-pill" id="mode-pill">Mode: Dark</span>
<div class="board" id="board">
<div class="a-logo"></div>
<div class="a-title">Welcome back</div>
<div class="a-sub">Sign in to continue to your workspace.</div>
<div class="a-field"></div>
<div class="a-field"></div>
<div class="a-btn">Sign in</div>
</div>
<span class="lint" id="lint">✓ lint_design · 0 issues</span>
<span class="flash" id="flash"></span>
</div>
</div>
</section>
<section class="block wrap" id="features">
<div class="section-head reveal">
<div class="eyebrow">What you get</div>
<h2 class="serif">A real design tool, <em>not a chat window</em> with a canvas bolted on.</h2>
<p>Frames, auto layout, components, variables and a hand-drawn whiteboard on one GPU-rendered canvas.</p>
</div>
<div class="features">
<article class="feature reveal">
<div class="copy">
<div class="eyebrow">The editor</div>
<h3 class="serif">Layers, canvas, inspector. <em>One native window.</em></h3>
<p>Built in Rust on egui and wgpu. Cached frames take about 3 ms with ten thousand visible layers.</p>
<ul class="ticks">
<li>Auto layout with gap, padding, hug, fill, wrap and constraints</li>
<li>Pen and Bézier editing, boolean operations, gradients and blend modes</li>
<li>A command palette over every action, page and layer</li>
</ul>
</div>
<div class="tilt"><div class="shot acrylic"><img src="media/hero.png" alt="PaperCraft with a habit-tracker design open: layers on the left, three annotated screens on the canvas, the inspector on the right" loading="lazy"></div></div>
</article>
<article class="feature reveal">
<div class="copy">
<div class="eyebrow">Chat mode</div>
<h3 class="serif">Pick an agent. <em>Describe the screen.</em></h3>
<p>A conversation sits next to the canvas. Point at layers with @ mentions or a picked area, and the agent gets a crop with your notes drawn on it.</p>
<ul class="ticks">
<li>Claude Code and Codex through their CLIs, with your own logins</li>
<li>Any ACP agent on your PATH, or the Anthropic API with your key</li>
<li>Every run can be reviewed, reverted one change at a time, or accepted</li>
</ul>
</div>
<div class="tilt"><div class="shot acrylic"><img src="media/chat.png" alt="Chat mode: a conversation with an agent beside the canvas" loading="lazy"></div></div>
</article>
<article class="feature reveal">
<div class="copy">
<div class="eyebrow">Annotations</div>
<h3 class="serif">Annotate <em>in place.</em> Agents read the notes.</h3>
<p>Pin typed notes for rules, behaviour, content and data to any layer, with a template per project. They travel with the design into the handoff spec.</p>
</div>
<div class="tilt"><div class="shot acrylic"><img src="media/annotations.png" alt="The Annotations view listing notes grouped by screen, with their category chips" loading="lazy"></div></div>
</article>
<article class="feature reveal">
<div class="copy">
<div class="eyebrow">Design systems</div>
<h3 class="serif">Tokens out as CSS, DTCG or Tailwind. <em>Code out as JSX.</em></h3>
<p>Variables with modes and aliases, components that work across projects, and shared libraries you publish, enable and update.</p>
<ul class="ticks">
<li>Light and dark are modes on one artboard, never copies</li>
<li>Lint for contrast, the 4px grid, type scale and touch targets, with auto-fix</li>
</ul>
</div>
<div class="tilt"><div class="shot acrylic"><img src="media/variables.png" alt="The Variables modal with Light and Dark values for brand, foreground and background" loading="lazy"></div></div>
</article>
</div>
</section>
<section class="block wrap" aria-labelledby="more-title">
<div class="section-head reveal">
<div class="eyebrow">And the rest</div>
<h2 class="serif" id="more-title">Sketch it, <em>present it,</em> ship it.</h2>
</div>
<div class="bento stagger">
<article class="tile wide acrylic lit reveal">
<div class="art">
<svg class="sketch" viewBox="0 0 360 92" width="100%" height="92" preserveAspectRatio="xMinYMid meet" aria-hidden="true">
<path d="M12 18 C60 12 110 16 140 19 C143 40 141 60 138 76 C96 79 50 78 14 74 C11 55 13 36 12 18Z"/>
<path d="M146 46 C180 44 210 46 236 47 M226 38 L238 47 L226 56"/>
<rect class="note" x="252" y="14" width="78" height="64" rx="4" transform="rotate(3 291 46)"/>
</svg>
</div>
<h3>Whiteboard mode</h3>
<p>Hand-drawn shapes, arrows bound to shapes, elbow connectors, sticky notes, freehand ink and a laser pointer, on the same canvas as your screens.</p>
</article>
<article class="tile acrylic lit reveal">
<div class="art"><span class="fmt in">.fig</span><span class="fmt in">.excalidraw</span><span class="arrow-dash"></span><span class="fmt out">PDF</span></div>
<h3>Import and export</h3>
<p>Figma, Excalidraw and SVG in. PNG at 1 to 4x, SVG and vector PDF out, for a selection, a frame or a page.</p>
</article>
<article class="tile acrylic lit reveal">
<div class="art"><div class="versions"><div>Agent run · Sign in</div><div>Auto-version · 14:02</div><div>Before handoff</div></div></div>
<h3>Versions and runs</h3>
<p>Named and automatic versions per page. An agent run saves one before its first edit, so you can always go back.</p>
</article>
<article class="tile acrylic lit reveal">
<div class="art"><span class="fmt">Welcome</span><span class="arrow-dash"></span><span class="fmt">Today</span></div>
<h3>Present mode</h3>
<p>Prototype links between frames, followed with a camera glide.</p>
</article>
<article class="tile acrylic lit reveal">
<div class="art"><div class="peers"><span style="background:var(--mauve)">M</span><span style="background:var(--blue)">A</span><span style="background:var(--green)">J</span></div></div>
<h3>Shared projects</h3>
<p>An optional relay merges concurrent edits to a page. Without one, nothing leaves your machine.</p>
</article>
<article class="tile wide acrylic lit reveal">
<div class="art">
<div class="swatches" aria-hidden="true">
<span style="background:#eff1f5"></span><span style="background:#303446"></span><span style="background:#24273a"></span><span style="background:#1e1e2e"></span><span style="background:#11111b"></span>
</div>
</div>
<h3>Themes</h3>
<p>System, Light and Dark, plus the four Catppuccin flavours: Latte, Frappé, Macchiato and Mocha.</p>
</article>
<article class="tile acrylic lit reveal">
<div class="art"><span class="fmt">wasm32</span><span class="arrow-dash"></span><span class="fmt out">browser</span></div>
<h3>Web build</h3>
<p>The same editor compiled to WebAssembly, served by <code>papercraft-relay</code>.</p>
</article>
</div>
</section>
<section class="block wrap" id="local">
<div class="section-head reveal">
<div class="eyebrow">Local-first</div>
<h2 class="serif">Your project is <em>a folder on your disk.</em></h2>
<p>Plain JSON you can back up, diff and commit. No account to make and no server to trust.</p>
</div>
<div class="local">
<div class="tree acrylic reveal">
<pre><span class="path">~/Documents/papercraft/</span><span class="d">Pulse/</span>
├── project.json <span class="c"># manifest, variables, components</span>
├── <span class="d">pages/</span> <span class="c"># one JSON file per page</span>
├── <span class="d">versions/</span> <span class="c"># named and automatic versions</span>
├── <span class="d">runs/</span> <span class="c"># agent runs, for review and revert</span>
└── <span class="d">chats/</span> <span class="c"># chat threads with their tool calls</span></pre>
</div>
<div class="points stagger">
<div class="point acrylic lit reveal"><h3>Atomic autosave</h3><p>Every write is atomic, and older files are migrated forward when they load.</p></div>
<div class="point acrylic lit reveal"><h3>Works with the app closed</h3><p><code>papercraft-cli --offline</code> runs the same tools on the project store, screenshots included.</p></div>
<div class="point acrylic lit reveal"><h3>Keys stay in your OS</h3><p>API keys live in the system credential store and never in a project file, chat or log.</p></div>
</div>
</div>
</section>
<section class="block wrap" id="agents">
<div class="section-head reveal">
<div class="eyebrow">Connect</div>
<h2 class="serif">Same tools, <em>three ways in.</em></h2>
<p>The MCP server is built into the app and listens on loopback only. Browser requests and foreign hosts are rejected.</p>
</div>
<div class="agents stagger">
<div class="card acrylic lit reveal">
<div class="stat" id="tool-count" data-to="98">98</div>
<h3>MCP tools, one undo step each</h3>
<div class="ways">
<div class="way"><b>MCP over HTTP</b><span>Built into the app at <code>127.0.0.1:29980/mcp</code>.</span></div>
<div class="way"><b>MCP over stdio</b><span><code>papercraft-mcp</code> relays to the running app.</span></div>
<div class="way"><b>Shell</b><span><code>papercraft-cli</code> calls the same tools, so the agent needs no schemas in context.</span></div>
</div>
</div>
<div class="card acrylic lit reveal">
<h3>Register it once</h3>
<p>Start PaperCraft, then add it to your agent.</p>
<div class="code">
<button class="copy-btn" type="button" data-copy="claude mcp add --transport http --scope user papercraft http://127.0.0.1:29980/mcp">Copy</button>
<pre><span class="c"># Claude Code</span>
<span class="k">claude</span> mcp add --transport http --scope user \
papercraft http://127.0.0.1:29980/mcp</pre>
</div>
<div class="code">
<button class="copy-btn" type="button" data-copy="codex mcp add papercraft --url http://127.0.0.1:29980/mcp">Copy</button>
<pre><span class="c"># Codex</span>
<span class="k">codex</span> mcp add papercraft --url http://127.0.0.1:29980/mcp</pre>
</div>
<div class="code">
<button class="copy-btn" type="button" data-copy="papercraft-cli call get_screenshot '{&quot;nodeId&quot;:&quot;abc123:1&quot;}' --project Demo -o card.png">Copy</button>
<pre><span class="c"># No app running</span>
<span class="k">papercraft-cli</span> call get_screenshot '{"nodeId":"abc123:1"}' \
--project Demo -o card.png</pre>
</div>
</div>
</div>
</section>
<section class="block wrap" aria-labelledby="keys-title">
<div class="section-head reveal">
<div class="eyebrow">Keyboard first</div>
<h2 class="serif" id="keys-title">The shortcuts <em>you'll use daily.</em></h2>
</div>
<div class="keys stagger">
<div class="key acrylic lit reveal"><kbd data-keys="mod K"></kbd><span>Command palette</span></div>
<div class="key acrylic lit reveal"><kbd data-keys="mod J"></kbd><span>Design or Chat mode</span></div>
<div class="key acrylic lit reveal"><kbd data-keys="mod alt shift V"></kbd><span>Variables</span></div>
<div class="key acrylic lit reveal"><kbd data-keys="N"></kbd><span>Annotate</span></div>
<div class="key acrylic lit reveal"><kbd data-keys="/"></kbd><span>Hide the side panels</span></div>
</div>
</section>
<section class="block wrap" id="download">
<div class="dl-head reveal">
<div class="section-head">
<div class="eyebrow">Download</div>
<h2 class="serif">Get the <em>latest build.</em></h2>
</div>
<a class="release-pill acrylic" id="release-link" href="https://github.com/nxtime/papercraft/releases/latest">
<span class="pulse" aria-hidden="true"></span>
<span><b data-version>latest</b> · <span id="release-date">GitHub releases</span></span>
</a>
</div>
<div class="platforms stagger" id="platforms">
<article class="platform acrylic">
<h3>Loading the latest release</h3>
<p class="note">All builds are also on the <a href="https://github.com/nxtime/papercraft/releases/latest">GitHub releases page</a>.</p>
</article>
</div>
<div class="dl-foot">
<div class="card acrylic reveal">
<h3>First run</h3>
<p>The Windows zip is unsigned, so SmartScreen may ask you to confirm. macOS builds are ad-hoc signed and not notarized: open the app with right-click, Open the first time.</p>
</div>
<div class="card acrylic reveal">
<h3>Build from source</h3>
<p>Rust 1.95 or newer, then <code>cargo build --release</code>. Linux needs Vulkan or GL and the usual X11 or Wayland libraries.</p>
</div>
</div>
</section>
<section class="wrap">
<div class="status acrylic reveal">
<div>
<div class="eyebrow">Status</div>
<h2 class="serif">Alpha, <em>and used daily.</em></h2>
<p>PaperCraft is good for screens, design systems and sketches, and it is not yet a replacement for a mature design tool. The roadmap lists what is missing, in priority order.</p>
</div>
<div class="actions">
<a class="btn btn-ghost" href="https://github.com/nxtime/papercraft/blob/main/ROADMAP.md">Roadmap</a>
<a class="btn btn-ghost" href="https://github.com/nxtime/papercraft#status-and-known-limits">Known limits</a>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap foot">
<span>PaperCraft is open source, licensed MIT or Apache-2.0.</span>
<nav aria-label="Links">
<a href="https://github.com/nxtime/papercraft">GitHub</a>
<a href="https://github.com/nxtime/papercraft/releases">Releases</a>
<a href="https://github.com/nxtime/papercraft/blob/main/docs/agents.md">Agent guide</a>
<a href="https://github.com/nxtime/papercraft/blob/main/CONTRIBUTING.md">Contributing</a>
</nav>
</div>
</footer>
<div class="toast acrylic" id="toast" role="status" aria-live="polite"></div>
<script src="site.js"></script>
</body>
</html>
+484
View File
@@ -0,0 +1,484 @@
/* The landing page sits on the app's own dark canvas: acrylic panels over a dot grid lit by
lavender glows, with the palette of the app icon (Catppuccin Mocha). */
:root {
color-scheme: dark;
--crust: #11111b;
--base: #181825;
--mantle: #1e1e2e;
--surface: #313244;
--overlay: #45475a;
--text: #cdd6f4;
--subtext: #a6adc8;
--muted: #7f849c;
--mauve: #cba6f7;
--mauve-hi: #d9bcfa;
--mauve-deep: #b48ef0;
--on-mauve: #1e1e2e;
--green: #a6e3a1;
--blue: #89b4fa;
--peach: #fab387;
--glass: rgb(30 30 46 / 0.55);
--glass-strong: rgb(30 30 46 / 0.8);
--glass-edge: rgb(205 214 244 / 0.09);
--glass-hi: rgb(255 255 255 / 0.06);
--glow: rgb(203 166 247 / 0.32);
--dot: rgb(205 214 244 / 0.07);
--well: rgb(17 17 27 / 0.6);
--display: "Instrument Serif", "Iowan Old Style", Georgia, serif;
--sans: "Geist", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--r-panel: 22px;
--r-control: 12px;
--wrap: 1180px;
--ease: cubic-bezier(.2, .7, .2, 1);
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
background: var(--crust);
color: var(--text);
font-family: var(--sans);
font-size: 16px;
line-height: 1.6;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
body::before {
content: ""; position: fixed; inset: 0 0 auto; height: env(safe-area-inset-top, 0px);
z-index: 30; background: var(--crust);
}
img { max-width: 100%; }
code { font-family: var(--mono); font-size: .92em; }
a { color: inherit; }
:focus-visible { outline: 2px solid var(--mauve); outline-offset: 3px; border-radius: 6px; }
/* The canvas behind everything: dot grid, drifting glows and a spotlight that follows the pointer */
.stage { position: fixed; inset: 0; z-index: -1; pointer-events: none; background: var(--crust); overflow: hidden; }
.stage::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 0) 0 0 / 22px 22px;
mask-image: radial-gradient(70rem 50rem at 50% 0%, #000 30%, transparent 85%);
}
.glow { position: absolute; border-radius: 50%; filter: blur(80px); opacity: .55; }
.glow.a { width: 52rem; height: 34rem; left: 50%; top: -18rem; margin-left: -26rem; background: rgb(203 166 247 / .32); }
.glow.b { width: 34rem; height: 26rem; right: -10rem; top: 45%; background: rgb(137 180 250 / .12); }
.glow.c { width: 30rem; height: 24rem; left: -10rem; bottom: -6rem; background: rgb(203 166 247 / .14); }
.spot {
position: absolute; inset: 0;
background: radial-gradient(28rem 28rem at var(--mx, 50%) var(--my, -20%), rgb(203 166 247 / .07), transparent 70%);
}
.wrap {
width: 100%; max-width: var(--wrap); margin-inline: auto;
padding-left: max(20px, env(safe-area-inset-left, 0px));
padding-right: max(20px, env(safe-area-inset-right, 0px));
}
/* Acrylic: translucent fill, backdrop blur, a hairline edge and a top highlight */
.acrylic {
position: relative;
background: linear-gradient(180deg, var(--glass-hi), transparent 40%), var(--glass);
backdrop-filter: blur(22px) saturate(160%);
-webkit-backdrop-filter: blur(22px) saturate(160%);
border: 1px solid var(--glass-edge);
box-shadow: 0 1px 0 var(--glass-hi) inset, 0 24px 60px -24px rgb(0 0 0 / .6);
}
/* Cards light up under the pointer, like a hovered panel in the app */
.lit::after {
content: ""; position: absolute; inset: -1px; border-radius: inherit; pointer-events: none;
background: radial-gradient(22rem 22rem at var(--lx, 50%) var(--ly, 50%), rgb(203 166 247 / .14), transparent 60%);
opacity: 0; transition: opacity .4s;
}
.lit:hover::after { opacity: 1; }
.eyebrow {
font-family: var(--mono); font-size: 12px; letter-spacing: .18em;
text-transform: uppercase; color: var(--muted);
}
.serif { font-family: var(--display); font-weight: 400; letter-spacing: -.01em; }
em { font-style: italic; color: var(--mauve); }
/* Nav */
header.nav { position: sticky; top: env(safe-area-inset-top, 0px); z-index: 20; padding-block: 14px; }
.nav-bar {
display: flex; align-items: center; gap: 16px; padding: 8px 8px 8px 14px; border-radius: 999px;
transition: background .3s, box-shadow .3s;
}
.scrolled .nav-bar { background: var(--glass-strong); }
.brand { display: flex; align-items: center; gap: 10px; text-decoration: none; font-weight: 600; }
.brand img { width: 28px; height: 28px; }
.nav-links { display: flex; gap: 4px; margin-left: auto; }
.nav-links a {
text-decoration: none; color: var(--subtext); font-size: 14px;
padding: 6px 12px; border-radius: 999px; transition: color .2s, background .2s;
}
.nav-links a:hover, .nav-links a.active { color: var(--text); background: var(--glass-hi); }
.progress {
position: absolute; left: 24px; right: 24px; bottom: -1px; height: 1px; border-radius: 1px;
background: linear-gradient(90deg, transparent, var(--mauve), transparent);
transform-origin: 0 50%; transform: scaleX(var(--p, 0)); opacity: .7;
}
/* Buttons */
.btn {
position: relative; overflow: hidden;
display: inline-flex; align-items: center; gap: 10px; text-decoration: none;
font: 500 15px/1 var(--sans); padding: 13px 20px; border-radius: 999px;
border: 1px solid transparent; cursor: pointer; white-space: nowrap;
transition: transform .25s var(--ease), box-shadow .25s, background .25s;
}
.btn-primary {
background: linear-gradient(180deg, var(--mauve-hi), var(--mauve-deep)); color: var(--on-mauve);
box-shadow: 0 0 0 1px rgb(255 255 255 / .25) inset, 0 10px 30px -8px var(--glow);
}
.btn-primary::before {
content: ""; position: absolute; inset: 0; transform: translateX(-120%);
background: linear-gradient(100deg, transparent 30%, rgb(255 255 255 / .45) 50%, transparent 70%);
}
.btn-primary:hover { transform: translateY(-2px); box-shadow: 0 0 0 1px rgb(255 255 255 / .3) inset, 0 16px 40px -8px var(--glow); }
.btn-primary:hover::before { transition: transform .8s var(--ease); transform: translateX(120%); }
.btn-ghost { background: var(--glass); border-color: var(--glass-edge); color: var(--text); backdrop-filter: blur(12px); }
.btn-ghost:hover { background: var(--glass-strong); transform: translateY(-2px); }
.btn-sm { padding: 9px 14px; font-size: 14px; }
.btn svg { width: 16px; height: 16px; flex: none; }
.ver { font-family: var(--mono); font-size: 12px; opacity: .7; }
/* Hero */
.hero { text-align: center; padding-block: clamp(48px, 9vw, 104px) 36px; }
.hero-mark { width: 76px; height: 76px; margin: 0 auto 22px; display: block; filter: drop-shadow(0 12px 30px var(--glow)); }
.hero h1 { font-size: clamp(44px, 7.4vw, 92px); line-height: 1; margin: 18px auto 0; max-width: 14ch; text-wrap: balance; }
.hero h1 .w { display: inline-block; }
.lede { max-width: 58ch; margin: 22px auto 0; color: var(--subtext); font-size: clamp(16px, 1.6vw, 18px); text-wrap: pretty; }
.cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 32px; }
.facts {
display: flex; flex-wrap: wrap; justify-content: center; gap: 8px 22px; margin-top: 26px;
font-family: var(--mono); font-size: 12px; color: var(--muted); letter-spacing: .04em;
}
.facts span::before { content: ""; display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--green); margin-right: 8px; vertical-align: 2px; }
/* Video window: tilted back until it scrolls into view */
.showcase { padding-block: 24px 24px; perspective: 1600px; }
.window {
border-radius: var(--r-panel); padding: 10px;
transform: rotateX(var(--tilt, 10deg)) scale(var(--zoom, .96)); transform-origin: 50% 0;
box-shadow: 0 0 0 1px var(--glass-edge), 0 40px 120px -30px var(--glow), 0 30px 80px -30px rgb(0 0 0 / .8);
}
.chrome { display: flex; align-items: center; gap: 8px; padding: 4px 6px 10px; }
.chrome i { width: 11px; height: 11px; border-radius: 50%; background: var(--surface); }
.chrome span { margin-inline: auto; font-family: var(--mono); font-size: 12px; color: var(--muted); }
.window video { display: block; width: 100%; aspect-ratio: 16 / 9; border-radius: 14px; background: var(--base); }
.caption { text-align: center; color: var(--muted); font-size: 14px; margin: 18px 0 0; }
/* Agents marquee */
.marquee { padding-block: 40px 8px; }
.marquee p { text-align: center; margin: 0 0 18px; }
.track {
display: flex; overflow: hidden;
mask-image: linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent);
}
.belt { display: flex; gap: 12px; padding-right: 12px; flex: none; animation: belt 48s linear infinite; }
.track:hover .belt { animation-play-state: paused; }
.chip {
display: inline-flex; align-items: center; gap: 9px; padding: 9px 14px; border-radius: 999px;
font-size: 14px; color: var(--subtext); white-space: nowrap;
}
.chip i {
width: 16px; height: 16px; flex: none; background: currentColor;
-webkit-mask: var(--logo) center / contain no-repeat; mask: var(--logo) center / contain no-repeat;
}
.chip i.none { -webkit-mask: none; mask: none; border-radius: 4px; background: var(--overlay); }
@keyframes belt { to { transform: translateX(-100%); } }
/* Sections */
section.block { padding-block: clamp(56px, 8vw, 96px); }
.section-head { max-width: 720px; margin-bottom: 44px; }
.section-head h2 { font-size: clamp(36px, 5vw, 60px); line-height: 1.04; margin: 14px 0 0; text-wrap: balance; }
.section-head p { color: var(--subtext); margin: 16px 0 0; max-width: 60ch; }
/* Live agent demo */
.live { display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 6fr); gap: 0; border-radius: var(--r-panel); overflow: hidden; }
.log { padding: 22px; border-right: 1px solid var(--glass-edge); min-width: 0; display: flex; flex-direction: column; }
.log-head { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--subtext); margin-bottom: 14px; }
.log-head .dot { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2.4s infinite; }
.log-head .undo { margin-left: auto; font-family: var(--mono); font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; }
.calls { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; font: 13px/1.5 var(--mono); }
.call {
display: grid; grid-template-columns: 14px minmax(0, 1fr) auto; gap: 4px 10px; align-items: start;
padding: 9px 11px; border-radius: 10px; background: var(--well); border: 1px solid var(--glass-edge);
opacity: .28; transition: opacity .4s, border-color .4s, transform .4s var(--ease);
}
.call .st { width: 14px; height: 14px; margin-top: 3px; border-radius: 50%; border: 1.5px solid var(--overlay); }
.call b { font-weight: 500; color: var(--text); }
.call .arg { grid-column: 2 / span 2; color: var(--muted); font-size: 12px; overflow-wrap: anywhere; }
.call .ms { color: var(--muted); font-size: 12px; font-variant-numeric: tabular-nums; }
.call.run { opacity: 1; border-color: rgb(203 166 247 / .5); transform: translateX(4px); }
.call.run .st { border-color: var(--mauve); border-top-color: transparent; animation: spin .7s linear infinite; }
.call.done { opacity: 1; }
.call.done .st { border: 0; background: var(--green) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4 8.5l2.5 2.5L12 5.5' fill='none' stroke='%231e1e2e' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E") center / 12px no-repeat; }
.call:not(.done) .ms { visibility: hidden; }
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0% { box-shadow: 0 0 0 0 rgb(166 227 161 / .6); } 70%, 100% { box-shadow: 0 0 0 8px rgb(166 227 161 / 0); } }
.board-area {
position: relative; min-width: 0; display: grid; place-items: center; padding: 40px 24px;
background: radial-gradient(circle at 1px 1px, var(--dot) 1px, transparent 0) 0 0 / 18px 18px, rgb(17 17 27 / .35);
}
.board-label { position: absolute; top: 16px; left: 20px; font: 12px var(--mono); color: var(--muted); }
.mode-pill {
position: absolute; top: 12px; right: 16px; font: 500 12px var(--mono); padding: 4px 10px; border-radius: 999px;
background: var(--well); border: 1px solid var(--glass-edge); color: var(--subtext); transition: color .4s;
}
.board {
--a-bg: #1e1e2e; --a-surface: #313244; --a-text: #cdd6f4; --a-sub: #a6adc8; --a-accent: #cba6f7; --a-on: #1e1e2e;
width: min(100%, 300px); aspect-ratio: 3 / 4; border-radius: 18px; padding: 26px 22px;
display: flex; flex-direction: column; gap: 12px;
outline: 1.5px dashed transparent; outline-offset: 6px;
background: transparent; transition: background .6s, outline-color .4s, box-shadow .6s;
}
.board.light { --a-bg: #eff1f5; --a-surface: #dce0e8; --a-text: #4c4f69; --a-sub: #6c6f85; --a-accent: #8839ef; --a-on: #eff1f5; }
.board.s1 { outline-color: rgb(203 166 247 / .6); }
.board.s2 { background: var(--a-bg); box-shadow: 0 30px 60px -30px rgb(0 0 0 / .8); }
.board > * { opacity: 0; transform: translateY(8px); transition: opacity .45s var(--ease), transform .45s var(--ease), background .6s, color .6s; }
.board.s3 > * { opacity: 1; transform: none; }
.board.s3 > *:nth-child(2) { transition-delay: .06s; } .board.s3 > *:nth-child(3) { transition-delay: .12s; }
.board.s3 > *:nth-child(4) { transition-delay: .18s; } .board.s3 > *:nth-child(5) { transition-delay: .24s; }
.board.s3 > *:nth-child(6) { transition-delay: .3s; }
.a-logo { width: 34px; height: 34px; border-radius: 10px; background: var(--a-surface); }
.a-title { font: 600 22px/1.2 var(--sans); color: var(--a-text); }
.a-sub { font-size: 12px; color: var(--a-sub); margin-top: -6px; }
.a-field { height: 36px; border-radius: 9px; background: var(--a-surface); }
.a-btn {
height: 38px; border-radius: 10px; margin-top: auto; display: grid; place-items: center;
font: 600 13px var(--sans); background: var(--a-surface); color: var(--a-sub);
}
.board.s4 .a-btn { background: var(--a-accent); color: var(--a-on); }
.board.s4 .a-logo { background: var(--a-accent); }
.flash { position: absolute; inset: 0; background: #fff; opacity: 0; pointer-events: none; }
.flash.on { animation: flash .5s ease-out; }
@keyframes flash { 0% { opacity: .35; } 100% { opacity: 0; } }
.lint {
position: absolute; bottom: 16px; left: 50%; display: inline-flex; align-items: center; gap: 8px;
padding: 7px 12px; border-radius: 999px; font: 500 12px var(--mono); color: var(--green);
background: var(--glass-strong); border: 1px solid rgb(166 227 161 / .35);
opacity: 0; transform: translate(-50%, 10px); transition: opacity .4s, transform .4s var(--ease);
}
.lint.on { opacity: 1; transform: translate(-50%, 0); }
/* Feature rows */
.features { display: grid; gap: 24px; }
.feature {
display: grid; grid-template-columns: minmax(0, 5fr) minmax(0, 7fr); gap: clamp(24px, 4vw, 56px);
align-items: center; padding-block: 28px;
}
.feature:nth-child(even) { grid-template-columns: minmax(0, 7fr) minmax(0, 5fr); }
.feature:nth-child(even) .copy { order: 2; }
.feature h3 { font-size: clamp(30px, 3.6vw, 44px); line-height: 1.06; margin: 12px 0 0; text-wrap: balance; }
.feature p { color: var(--subtext); margin: 14px 0 0; }
.ticks { margin: 16px 0 0; padding: 0; list-style: none; display: grid; gap: 8px; color: var(--subtext); font-size: 15px; }
.ticks li { display: flex; gap: 10px; }
.ticks li::before { content: ""; flex: none; width: 6px; height: 6px; margin-top: 10px; border-radius: 2px; background: var(--mauve); }
.tilt { perspective: 1200px; }
.shot {
border-radius: var(--r-panel); padding: 8px;
transform: rotateX(var(--rx, 0deg)) rotateY(var(--ry, 0deg)); transition: transform .5s var(--ease);
}
.shot img { display: block; width: 100%; height: auto; border-radius: 15px; }
/* Bento grid */
.bento { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.tile { border-radius: var(--r-panel); padding: 24px; min-width: 0; display: flex; flex-direction: column; gap: 10px; }
.tile.wide { grid-column: span 2; }
.tile h3 { margin: 0; font-size: 17px; font-weight: 600; }
.tile p { margin: 0; color: var(--subtext); font-size: 15px; }
.tile .art { height: 92px; margin-bottom: 8px; display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.fmt {
font: 500 12px var(--mono); padding: 6px 10px; border-radius: 8px; background: var(--well);
border: 1px solid var(--glass-edge); color: var(--subtext);
}
.fmt.in { color: var(--blue); } .fmt.out { color: var(--mauve); }
.arrow-dash { flex: 1; min-width: 30px; height: 1px; background: repeating-linear-gradient(90deg, var(--overlay) 0 6px, transparent 6px 12px); background-size: 24px 1px; animation: dash 1.2s linear infinite; }
@keyframes dash { to { background-position: 24px 0; } }
.swatches { display: flex; gap: 8px; }
.swatches span {
width: 40px; height: 56px; border-radius: 10px; border: 1px solid var(--glass-edge);
transition: transform .4s var(--ease);
}
.tile:hover .swatches span { transform: translateY(-6px); }
.tile:hover .swatches span:nth-child(2) { transition-delay: .04s; } .tile:hover .swatches span:nth-child(3) { transition-delay: .08s; }
.tile:hover .swatches span:nth-child(4) { transition-delay: .12s; } .tile:hover .swatches span:nth-child(5) { transition-delay: .16s; }
.sketch path { fill: none; stroke: var(--mauve); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 340; stroke-dashoffset: 340; }
.sketch .note { fill: var(--peach); opacity: .85; }
.in-view .sketch path { animation: draw 1.6s var(--ease) forwards; }
.in-view .sketch path:nth-of-type(2) { animation-delay: .4s; }
@keyframes draw { to { stroke-dashoffset: 0; } }
.versions { display: grid; gap: 6px; width: 100%; font: 12px var(--mono); color: var(--muted); }
.versions div { display: flex; gap: 10px; align-items: center; }
.versions div::before { content: ""; width: 8px; height: 8px; border-radius: 50%; border: 1.5px solid var(--overlay); flex: none; }
.versions div:first-child { color: var(--text); }
.versions div:first-child::before { border-color: var(--mauve); background: var(--mauve); }
.peers { display: flex; align-items: center; }
.peers span {
width: 38px; height: 38px; border-radius: 50%; display: grid; place-items: center; font: 600 13px var(--sans);
color: var(--on-mauve); margin-left: -8px; border: 2px solid var(--mantle);
}
.peers span:first-child { margin-left: 0; }
/* Local-first */
.local { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 20px; align-items: stretch; }
.tree { border-radius: var(--r-panel); padding: 22px 24px; font: 13px/2 var(--mono); color: var(--subtext); overflow-x: auto; min-width: 0; }
.tree .path { color: var(--muted); }
.tree .d { color: var(--mauve); }
.tree .c { color: var(--muted); }
.tree pre { margin: 0; }
.points { display: grid; gap: 14px; align-content: start; }
.point { border-radius: 16px; padding: 18px 20px; }
.point h3 { margin: 0; font-size: 16px; font-weight: 600; }
.point p { margin: 4px 0 0; color: var(--subtext); font-size: 15px; }
/* Agents */
.agents { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 20px; }
.card { border-radius: var(--r-panel); padding: 24px; min-width: 0; }
.card h3 { margin: 0; font-size: 17px; font-weight: 600; }
.card > p { color: var(--subtext); margin: 8px 0 0; font-size: 15px; }
.stat { font-family: var(--display); font-size: 72px; line-height: 1; color: var(--mauve); font-variant-numeric: tabular-nums; }
.ways { display: grid; gap: 14px; margin-top: 18px; }
.way { display: grid; grid-template-columns: 120px minmax(0, 1fr); gap: 12px; font-size: 14px; }
.way b { font-weight: 500; }
.way span { color: var(--subtext); }
.code {
position: relative; margin-top: 14px; border-radius: var(--r-control); background: var(--well);
border: 1px solid var(--glass-edge); font: 13px/1.7 var(--mono); color: var(--subtext);
}
.code pre { margin: 0; padding: 14px 16px; padding-right: 70px; overflow-x: auto; }
.code .c { color: var(--muted); }
.code .k { color: var(--mauve); }
.copy-btn {
position: absolute; top: 8px; right: 8px; font: 500 12px var(--sans); color: var(--subtext);
background: var(--glass-strong); border: 1px solid var(--glass-edge); border-radius: 8px;
padding: 5px 9px; cursor: pointer; transition: color .2s, border-color .2s;
}
.copy-btn:hover { color: var(--text); border-color: rgb(203 166 247 / .4); }
/* Shortcuts */
.keys { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 12px; }
.key { border-radius: 16px; padding: 18px; display: grid; gap: 12px; justify-items: start; }
.key span { font-size: 14px; color: var(--subtext); }
kbd {
display: inline-flex; gap: 4px; font: 500 14px var(--mono); color: var(--text);
}
kbd b {
min-width: 30px; height: 30px; padding: 0 8px; display: grid; place-items: center; font-weight: 500;
border-radius: 8px; background: var(--well); border: 1px solid var(--glass-edge);
box-shadow: 0 2px 0 rgb(0 0 0 / .4); transition: transform .15s, box-shadow .15s;
}
.key:hover kbd b { transform: translateY(2px); box-shadow: 0 0 0 rgb(0 0 0 / .4); }
/* Download */
.dl-head { display: flex; flex-wrap: wrap; align-items: end; justify-content: space-between; gap: 16px; margin-bottom: 28px; }
.dl-head .section-head { margin: 0; }
.release-pill {
display: inline-flex; align-items: center; gap: 10px; padding: 8px 14px; border-radius: 999px;
font-family: var(--mono); font-size: 13px; color: var(--subtext); text-decoration: none;
}
.release-pill b { color: var(--text); font-weight: 500; }
.pulse { width: 8px; height: 8px; border-radius: 50%; background: var(--green); animation: pulse 2.4s infinite; }
.platforms { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.platform { display: flex; flex-direction: column; gap: 12px; border-radius: var(--r-panel); padding: 22px; min-width: 0; }
.platform.mine { box-shadow: 0 0 0 1px rgb(203 166 247 / .55), 0 0 50px -10px var(--glow); }
.platform .tag {
position: absolute; top: 18px; right: 18px; font: 500 11px var(--mono); letter-spacing: .08em; text-transform: uppercase;
color: var(--on-mauve); background: var(--mauve); padding: 3px 8px; border-radius: 999px;
}
.platform .os { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; }
.platform .os svg { width: 28px; height: 28px; color: var(--text); flex: none; }
.platform h3 { margin: 0; font-size: 18px; font-weight: 600; }
.platform .arch { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.file { display: grid; grid-template-columns: minmax(0, 1fr) auto; border-radius: var(--r-control); background: var(--well); border: 1px solid var(--glass-edge); transition: border-color .2s; }
.file:hover { border-color: rgb(203 166 247 / .5); }
.file a { display: grid; gap: 2px; padding: 12px 14px; text-decoration: none; min-width: 0; }
.file .fname { font: 500 14px var(--sans); }
.file .fmeta { font-size: 12px; color: var(--muted); font-variant-numeric: tabular-nums; font-family: var(--mono); }
.file .sha {
align-self: center; margin-right: 8px; font: 500 11px var(--mono); color: var(--muted); cursor: pointer;
background: transparent; border: 1px solid var(--glass-edge); border-radius: 7px; padding: 5px 8px;
}
.file .sha:hover { color: var(--text); }
.platform .note { font-size: 13px; color: var(--muted); margin: auto 0 0; }
.dl-foot { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); gap: 16px; margin-top: 16px; }
.dl-foot .card p { margin: 8px 0 0; color: var(--subtext); font-size: 14px; }
/* Status */
.status { border-radius: var(--r-panel); padding: clamp(24px, 4vw, 40px); display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 24px; align-items: center; }
.status h2 { font-size: clamp(30px, 4vw, 44px); line-height: 1.06; margin: 10px 0 0; }
.status p { color: var(--subtext); margin: 12px 0 0; max-width: 62ch; }
.status .actions { display: flex; gap: 12px; flex-wrap: wrap; }
footer { padding-block: 40px; border-top: 1px solid var(--glass-edge); margin-top: 56px; }
.foot { display: flex; flex-wrap: wrap; gap: 16px 28px; align-items: center; justify-content: space-between; color: var(--muted); font-size: 14px; }
.foot nav { display: flex; gap: 20px; flex-wrap: wrap; }
.foot a { text-decoration: none; }
.foot a:hover { color: var(--text); }
.toast {
position: fixed; left: 50%; bottom: calc(24px + env(safe-area-inset-bottom, 0px)); z-index: 40;
padding: 10px 16px; border-radius: 999px; font-size: 14px; transform: translate(-50%, 20px); opacity: 0;
transition: opacity .3s, transform .3s var(--ease); pointer-events: none;
}
.toast.on { opacity: 1; transform: translate(-50%, 0); }
/* Motion. Everything is readable at rest; motion only moves it into place. */
@media (prefers-reduced-motion: no-preference) {
.glow.a { animation: drift-a 22s ease-in-out infinite alternate; }
.glow.b { animation: drift-b 28s ease-in-out infinite alternate; }
.glow.c { animation: drift-c 26s ease-in-out infinite alternate; }
.hero-mark { animation: float 6s ease-in-out infinite; }
.rise { animation: rise .9s var(--ease) both; }
.hero h1 .w { animation: word .9s var(--ease) both; animation-delay: calc(var(--i) * 60ms + 120ms); }
.d1 { animation-delay: .1s; } .d2 { animation-delay: .45s; } .d3 { animation-delay: .6s; }
.js .reveal { opacity: .001; transform: translateY(24px); transition: opacity .8s var(--ease), transform .8s var(--ease); }
.js .reveal.in-view { opacity: 1; transform: none; }
.js .stagger > .reveal:nth-child(2) { transition-delay: .08s; }
.js .stagger > .reveal:nth-child(3) { transition-delay: .16s; }
.js .stagger > .reveal:nth-child(4) { transition-delay: .24s; }
.js .stagger > .reveal:nth-child(5) { transition-delay: .32s; }
.js .stagger > .reveal:nth-child(6) { transition-delay: .4s; }
@keyframes rise { from { opacity: 0; transform: translateY(14px); } to { opacity: 1; transform: none; } }
@keyframes word { from { opacity: 0; transform: translateY(.4em); filter: blur(6px); } to { opacity: 1; transform: none; filter: none; } }
@keyframes float { 50% { transform: translateY(-6px); } }
@keyframes drift-a { to { transform: translate(6rem, 4rem) scale(1.1); } }
@keyframes drift-b { to { transform: translate(-8rem, -6rem) scale(1.15); } }
@keyframes drift-c { to { transform: translate(10rem, -4rem); } }
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
.window { --tilt: 0deg; --zoom: 1; }
.belt { animation: none; }
.track { overflow-x: auto; }
.pulse, .log-head .dot, .arrow-dash { animation: none; }
.sketch path { stroke-dashoffset: 0; }
}
@media (max-width: 960px) {
.feature, .feature:nth-child(even) { grid-template-columns: minmax(0, 1fr); }
.feature:nth-child(even) .copy { order: 0; }
.agents, .platforms, .local, .dl-foot, .live { grid-template-columns: minmax(0, 1fr); }
.log { border-right: 0; border-bottom: 1px solid var(--glass-edge); }
.bento { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.keys { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.status { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 640px) {
.nav-links { display: none; }
.nav-bar .btn { margin-left: auto; }
.way { grid-template-columns: minmax(0, 1fr); gap: 2px; }
.window { --tilt: 0deg; --zoom: 1; padding: 6px; }
.chrome { display: none; }
.bento { grid-template-columns: minmax(0, 1fr); }
.tile.wide { grid-column: auto; }
.keys { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
+347
View File
@@ -0,0 +1,347 @@
const REPO = 'nxtime/papercraft';
const REDUCED = matchMedia('(prefers-reduced-motion: reduce)').matches;
const ICONS = {
windows: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M3 5.5 10.5 4.4v7.1H3V5.5Zm0 13 7.5 1.1v-7H3v5.9Zm8.4 1.2L21 21V12.6h-9.6v7.1Zm0-15.4v7.2H21V3l-9.6 1.3Z"/></svg>',
macos: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16.4 12.6c0-2.4 2-3.6 2.1-3.7-1.1-1.7-2.9-1.9-3.5-1.9-1.5-.2-2.9.9-3.7.9-.8 0-1.9-.9-3.2-.8-1.6 0-3.1 1-4 2.4-1.7 3-.4 7.3 1.2 9.7.8 1.2 1.8 2.5 3 2.4 1.2 0 1.7-.8 3.1-.8 1.5 0 1.9.8 3.2.8 1.3 0 2.2-1.2 3-2.4.9-1.4 1.3-2.7 1.3-2.8 0 0-2.5-1-2.5-3.8ZM14 5.4c.7-.8 1.1-1.9 1-3-.9 0-2.1.6-2.8 1.4-.6.7-1.2 1.9-1 2.9 1.1.1 2.1-.5 2.8-1.3Z"/></svg>',
linux: '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2c-2 0-3.3 1.7-3.3 4.2 0 1.4.4 2.4-.4 3.7C7 12 5.5 14 5.5 16.3c0 .6.1 1.1.4 1.6-.9.4-2 .9-2 1.8 0 1.2 1.9 1.3 3.5 1.8 1.3.4 2 .5 2.6-.3.4.1 1.2.2 2 .2s1.6-.1 2-.2c.6.8 1.3.7 2.6.3 1.6-.5 3.5-.6 3.5-1.8 0-.9-1.1-1.4-2-1.8.3-.5.4-1 .4-1.6 0-2.3-1.5-4.3-2.8-6.4-.8-1.3-.4-2.3-.4-3.7C15.3 3.7 14 2 12 2Zm-1.4 4c.4 0 .7.5.7 1.1s-.3 1-.7 1-.7-.4-.7-1 .3-1.1.7-1.1Zm2.8 0c.4 0 .7.5.7 1.1s-.3 1-.7 1-.7-.4-.7-1 .3-1.1.7-1.1ZM12 9c.9 0 2 .6 2 1s-1.1 1-2 1-2-.6-2-1 1.1-1 2-1Z"/></svg>'
};
const PLATFORMS = [
{ key: 'windows', name: 'Windows', arch: 'x86_64', note: 'App, CLI, MCP bridge and relay in one zip.' },
{ key: 'macos', name: 'macOS', arch: 'Apple silicon', note: 'PaperCraft.app in a disk image, plus the CLI tools.' },
{ key: 'linux', name: 'Linux', arch: 'x86_64 · X11 and Wayland', note: 'Needs Vulkan or GL and the usual X11 libraries.' }
];
const OS = (() => {
const ua = navigator.userAgent;
if (/Windows/i.test(ua)) return 'windows';
if (/Macintosh|Mac OS X/i.test(ua) && !/iPhone|iPad/i.test(ua)) return 'macos';
if (/Linux/i.test(ua) && !/Android/i.test(ua)) return 'linux';
return null;
})();
const toast = message => {
const el = document.getElementById('toast');
el.textContent = message;
el.classList.add('on');
clearTimeout(toast.timer);
toast.timer = setTimeout(() => el.classList.remove('on'), 1800);
};
const copy = (text, label) => navigator.clipboard.writeText(text)
.then(() => toast(`${label} copied`))
.catch(() => toast('Copy was blocked. Select the text instead.'));
const escapeHtml = s => s.replace(/[&<>"']/g, c => `&#${c.charCodeAt(0)};`);
/* Release: release.json is written by the site workflow; the GitHub API is the fallback. */
const formatSize = bytes => `${(bytes / 1048576).toFixed(1)} MB`;
const formatDate = iso => new Date(iso).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
const fileType = name => (name.endsWith('.tar.gz') ? 'tar.gz' : name.split('.').pop());
const fileLabel = name => (name.includes('-cli-') ? 'CLI tools' : 'Desktop app');
const loadRelease = async () => {
try {
const res = await fetch('release.json', { cache: 'no-cache' });
if (res.ok) return await res.json();
} catch (err) {
console.warn('release.json is unavailable, asking the GitHub API', err);
}
const res = await fetch(`https://api.github.com/repos/${REPO}/releases/latest`);
if (!res.ok) throw new Error(`The GitHub API answered ${res.status}`);
const r = await res.json();
return {
tag: r.tag_name,
name: r.name,
publishedAt: r.published_at,
url: r.html_url,
assets: r.assets.map(a => ({ name: a.name, size: a.size, url: a.browser_download_url, digest: a.digest || null }))
};
};
const renderRelease = release => {
const grid = document.getElementById('platforms');
document.querySelectorAll('[data-version]').forEach(el => { el.textContent = release.tag; });
document.getElementById('release-date').textContent = formatDate(release.publishedAt);
document.getElementById('release-link').href = release.url;
grid.innerHTML = PLATFORMS.map(p => {
const files = release.assets.filter(a => a.name.includes(`-${p.key}-`));
const mine = p.key === OS;
const rows = files.map(a => `
<div class="file">
<a href="${escapeHtml(a.url)}">
<span class="fname">${fileLabel(a.name)}</span>
<span class="fmeta">${fileType(a.name)} · ${formatSize(a.size)}</span>
</a>
${a.digest ? `<button class="sha" type="button" data-digest="${escapeHtml(a.digest)}" title="Copy the SHA-256 checksum">SHA-256</button>` : ''}
</div>`).join('');
return `
<article class="platform acrylic lit reveal${mine ? ' mine' : ''}">
${mine ? '<span class="tag">Your system</span>' : ''}
<div class="os">${ICONS[p.key]}<div><h3>${p.name}</h3><div class="arch">${p.arch}</div></div></div>
${rows || '<p class="note">No build in this release. Build from source.</p>'}
<p class="note">${p.note}</p>
</article>`;
}).join('');
grid.querySelectorAll('.sha').forEach(btn => {
btn.addEventListener('click', () => copy(btn.dataset.digest.replace(/^sha256:/, ''), 'Checksum'));
});
grid.querySelectorAll('.reveal').forEach(el => revealer.observe(el));
grid.querySelectorAll('.lit').forEach(light);
const primary = OS && release.assets.find(a => a.name.includes(`-${OS}-`) && !a.name.includes('-cli-'));
if (primary) {
document.getElementById('hero-dl').href = primary.url;
document.getElementById('hero-dl-label').textContent = `Download for ${PLATFORMS.find(p => p.key === OS).name}`;
}
};
const showReleaseError = err => {
console.error(err);
document.getElementById('platforms').innerHTML = `
<article class="platform acrylic">
<h3>The release list did not load</h3>
<p class="note">Every build is on the GitHub releases page.</p>
<a class="btn btn-ghost" href="https://github.com/${REPO}/releases/latest">Open releases</a>
</article>`;
};
/* Motion */
const revealer = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
e.target.classList.add('in-view');
revealer.unobserve(e.target);
});
}, { rootMargin: '0px 0px -8% 0px' });
const light = el => {
el.addEventListener('pointermove', e => {
const r = el.getBoundingClientRect();
el.style.setProperty('--lx', `${e.clientX - r.left}px`);
el.style.setProperty('--ly', `${e.clientY - r.top}px`);
});
};
const splitHeadline = () => {
const h1 = document.getElementById('headline');
let i = 0;
const wrap = node => {
if (node.nodeType === Node.TEXT_NODE) {
const frag = document.createDocumentFragment();
node.textContent.split(/(\s+)/).forEach(part => {
if (!part) return;
if (/^\s+$/.test(part)) { frag.append(part); return; }
const span = document.createElement('span');
span.className = 'w';
span.style.setProperty('--i', i++);
span.textContent = part;
frag.append(span);
});
node.replaceWith(frag);
return;
}
[...node.childNodes].forEach(wrap);
};
[...h1.childNodes].forEach(wrap);
};
const onScroll = () => {
const y = scrollY;
const max = document.documentElement.scrollHeight - innerHeight;
const win = document.getElementById('window');
const rect = win.getBoundingClientRect();
const t = Math.min(1, Math.max(0, 1 - rect.top / (innerHeight * 0.8)));
document.getElementById('nav').classList.toggle('scrolled', y > 24);
document.querySelector('.progress').style.setProperty('--p', max > 0 ? (y / max).toFixed(4) : 0);
if (!REDUCED && innerWidth > 640) {
win.style.setProperty('--tilt', `${(10 * (1 - t)).toFixed(2)}deg`);
win.style.setProperty('--zoom', (0.96 + 0.04 * t).toFixed(4));
}
};
const tiltShots = () => {
document.querySelectorAll('.tilt').forEach(box => {
const shot = box.querySelector('.shot');
box.addEventListener('pointermove', e => {
const r = box.getBoundingClientRect();
const x = (e.clientX - r.left) / r.width - 0.5;
const y = (e.clientY - r.top) / r.height - 0.5;
shot.style.setProperty('--ry', `${(x * 6).toFixed(2)}deg`);
shot.style.setProperty('--rx', `${(-y * 6).toFixed(2)}deg`);
});
box.addEventListener('pointerleave', () => {
shot.style.setProperty('--ry', '0deg');
shot.style.setProperty('--rx', '0deg');
});
});
};
const countUp = el => {
const to = Number(el.dataset.to);
const start = performance.now();
const step = now => {
const k = Math.min(1, (now - start) / 1200);
el.textContent = Math.round(to * (1 - Math.pow(1 - k, 3)));
if (k < 1) requestAnimationFrame(step);
};
requestAnimationFrame(step);
};
/* The live agent demo: each tool call lights up, then the artboard shows its effect. */
const runDemo = () => {
const calls = [...document.querySelectorAll('#calls .call')];
const board = document.getElementById('board');
const pill = document.getElementById('mode-pill');
const undo = document.getElementById('undo-count');
const flash = document.getElementById('flash');
const lint = document.getElementById('lint');
const effects = [
() => board.classList.add('s1'),
() => board.classList.add('s2'),
() => board.classList.add('s3'),
() => board.classList.add('s4'),
() => { board.classList.add('light'); pill.textContent = 'Mode: Light'; },
() => { flash.classList.remove('on'); void flash.offsetWidth; flash.classList.add('on'); },
() => lint.classList.add('on')
];
let step = 0;
let timer = null;
const reset = () => {
step = 0;
calls.forEach(c => c.classList.remove('run', 'done'));
board.className = 'board';
pill.textContent = 'Mode: Dark';
lint.classList.remove('on');
undo.textContent = '0 undo steps';
};
const tick = () => {
if (step === calls.length) {
timer = setTimeout(() => { reset(); tick(); }, 3200);
return;
}
const call = calls[step];
call.classList.add('run');
timer = setTimeout(() => {
call.classList.replace('run', 'done');
effects[step]();
step++;
undo.textContent = `${step} undo step${step === 1 ? '' : 's'}`;
timer = setTimeout(tick, 380);
}, 620);
};
if (REDUCED) {
calls.forEach((c, i) => { c.classList.add('done'); effects[i](); });
undo.textContent = `${calls.length} undo steps`;
return;
}
new IntersectionObserver(([e]) => {
if (e.isIntersecting && !timer) tick();
if (!e.isIntersecting && timer) { clearTimeout(timer); timer = null; reset(); }
}, { threshold: 0.35 }).observe(document.getElementById('live-demo'));
};
const keyLabels = () => {
const mac = OS === 'macos';
const names = mac ? { mod: '⌘', alt: '⌥', shift: '⇧' } : { mod: 'Ctrl', alt: 'Alt', shift: 'Shift' };
document.querySelectorAll('kbd[data-keys]').forEach(kbd => {
kbd.innerHTML = kbd.dataset.keys.split(' ').map(k => `<b>${names[k] || k}</b>`).join('');
});
};
const activeNav = () => {
const links = [...document.querySelectorAll('.nav-links a[href^="#"]')];
const spy = new IntersectionObserver(entries => {
entries.forEach(e => {
if (!e.isIntersecting) return;
links.forEach(a => a.classList.toggle('active', a.getAttribute('href') === `#${e.target.id}`));
});
}, { rootMargin: '-45% 0px -50% 0px' });
links.forEach(a => {
const target = document.querySelector(a.getAttribute('href'));
if (target) spy.observe(target);
});
};
/* Boot */
document.documentElement.classList.add('js');
splitHeadline();
keyLabels();
activeNav();
tiltShots();
runDemo();
document.querySelectorAll('.reveal').forEach(el => revealer.observe(el));
document.querySelectorAll('.lit').forEach(light);
document.querySelectorAll('.copy-btn').forEach(btn => {
btn.addEventListener('click', () => copy(btn.dataset.copy, 'Command'));
});
const belt = document.querySelector('.belt');
const twin = belt.cloneNode(true);
// A second copy makes the loop seamless; screen readers only need the first.
twin.setAttribute('aria-hidden', 'true');
belt.after(twin);
const counter = document.getElementById('tool-count');
new IntersectionObserver(([e], obs) => {
if (!e.isIntersecting) return;
if (!REDUCED) countUp(counter);
obs.disconnect();
}, { threshold: 0.6 }).observe(counter);
addEventListener('scroll', onScroll, { passive: true });
addEventListener('resize', onScroll);
onScroll();
addEventListener('pointermove', e => {
const spot = document.querySelector('.spot');
spot.style.setProperty('--mx', `${e.clientX}px`);
spot.style.setProperty('--my', `${e.clientY}px`);
}, { passive: true });
loadRelease().then(renderRelease).catch(showReleaseError);
+83 -153
View File
@@ -1,180 +1,110 @@
---
name: papercraft-critique
description: Pre-emit critique gate for work built in PaperCraft. Five panels (designer, critic, brand, accessibility, copy) score a screen 0-10 against PaperCraft's own objective evidence (lint_design, verify_code, get_screenshot, get_design_spec, get_variables), the weighted composite must reach 8.0, and a failed gate ships the best round instead of the last one. Use when an agent is about to hand over a PaperCraft screen and wants a tool-backed review instead of a self-praise.
description: Critique finished PaperCraft work the way a design team does - against the person's objective, through independent reviewers (the person's vision, UX heuristics, flow across screens, QA states, visual, accessibility, copy, an end user from the audience), each finding rated 0-4 and backed by tool evidence, then fixed, reported and, when the person approves, kept as a lesson. Use when finish_working_on_nodes offers a critique, when asked to review or critique a design, or before handing over a screen or flow.
---
# papercraft-critique
A gate, not a compliment. Before a screen leaves the app it goes through five panels. Each panel is a rubric pass over the same artifact, and each panel may only cite what a PaperCraft tool actually returned. The panels produce five scores, the scores produce one composite, and the composite decides whether the work ships. Mechanics live in the `papercraft` skill, taste in `papercraft-design`; this skill is the gate.
A critique judges a design against what it is for, not against taste. The tool `critique_design`
hands you the evidence and the reviewers; this skill is how to run them. Mechanics live in the
`papercraft` skill, taste in `papercraft-design`. Per-reviewer checklists:
[personas.md](references/personas.md).
Reference: [panels.md](references/panels.md) (what each panel inspects, which tool backs it, its 0–10 bands).
## 1. When
## 1. When the gate runs
* `finish_working_on_nodes` returns `next`: ask the person (`ask_user`) whether to critique now.
* The person asks "review this", "is it ready?", "critique it".
* Before handing over a whole screen or flow. Not for one component, an early draft or a copy tweak.
Run it when the resolver (section 2) says on, or when the request is a review of finished work:
## 2. Objective first
* a whole screen is about to be handed over: landing page, dashboard, sign-in, settings, an onboarding flow, a whiteboard plan;
* a redesign of an existing screen, after the edits and a clean `lint_design`;
* the user asked "does this look right?", "review this", "critique it", "is it ready to ship?".
Critique without an objective is opinion. Before judging, have:
Do not run it for: a single component or icon, an exploratory draft before a direction is chosen (the `papercraft-design` section 0 step), or a one-line copy tweak. The gate costs a lint and a screenshot per round; spend it on work that is finished.
* **Objective**: what the work must achieve ("a returning customer reorders in two taps").
* **Audience**: who uses it, in what context (phone, in a hurry, first visit, low vision).
* **Top tasks**: the two to five things people come to do.
If the gate is on for a run, it must produce a status. Never skip it silently: report `Degraded` with the reason.
Take them from the brief, the spec, the person's notes (`get_feedback`) and references. Missing?
Ask with `ask_user` and pass them to `critique_design {objective, audience}`.
## 2. Enable resolver
## 3. The reviewers
Four levels, highest priority first. Take the first level that gives an explicit answer; "unset" falls through to the next one. A higher level saying *off* is final — a lower level cannot turn the gate back on.
| Order | Level | PaperCraft equivalent | How to read it |
|---|---|---|---|
| 1 | Per-task policy | the instruction the agent was given for this run | "run the critique gate before you finish", "review this screen" |
| 2 | Per-project setting | a toggle stored in the project's own metadata | **not implemented today** — PaperCraft has no project-level flag. Treat this level as unset and fall through to the environment flag or the per-task instruction |
| 3 | Environment flag | `PAPERCRAFT_CRITIQUE_ENABLED` | `1` / `true` / `always` = on, `0` / `false` / `never` = off, unset = fall through |
| 4 | Rollout phase | the release phase of the skill itself | off by default: the gate ships disabled until a run or a project turns it on |
So today the gate turns on in one of two ways: the user asks for it in the run's instructions, or `PAPERCRAFT_CRITIQUE_ENABLED=1` is in the environment. A future project setting — stored with the project, not with the user — would sit between those two and win over the environment, so a project could opt in permanently without an env var.
## 3. Order of operations
Objective checks first, subjective panels second. A panel never scores above evidence that contradicts it.
One round is: **evidence → repair → judge → score**.
1. **Snapshot.** Note the `pageId` and the artboard `nodeId` under review and keep them for the whole gate. A page switch between rounds invalidates the evidence.
2. **Evidence.** `lint_design` (`nodeId`, `pageId`, no `fix` yet) for the raw findings — the payload counts `errors`, `warnings` and `info`, groups them in `byRule`, and lists each issue with its `rule`, `severity`, `node`, `message` and `fix`; `get_screenshot` of the same node (`scale: 2` to read small text); `get_variables` for the token set and the active mode; `get_design_spec` (`nodeId`, `target`) when the design is headed for code. If there is code to check, `verify_code` (`nodeId`, `target`, `code`).
3. **Repair the hard evidence.** Fix every `lint_design` error and warning and re-run the lint until it is clean; fix every `verify_code` violation until `passed` is true. A panel cannot be scored on a design that still carries a lint error.
4. **Judge the five panels** on the evidence from steps 2–3 plus the screenshot. Every finding cites the tool result or the screenshot region behind it.
5. **Score.** Five scores 0–10, then the composite (section 5). Composite ≥ 8.0 → status `Shipped`, stop, `finish_working_on_nodes`.
6. **Otherwise fix and go again.** Take the findings in composite-impact order, fix them, run round N+1. Maximum 3 rounds.
7. **Ship the best round** if round 3 is still below the threshold (section 6).
The panels are rubric passes by the agent that holds the tools: one agent, five passes, five different rubrics. A runtime that can spawn subagents may run the panels in parallel, but the evidence vetoes, the composite and the round counter belong to the coordinator.
## 4. Hard evidence vetoes a panel
`lint_design` and `verify_code` are not suggestions. Each finding belongs to one panel, and that panel cannot score above the band its evidence forces.
| Evidence | Rule | Owner panel | Cap |
|---|---|---|---|
| `lint_design` error | `contrast` | Accessibility (the Critic reads it too) | 3.9 |
| `lint_design` warning | `text-size` (under 12px) | Accessibility | 6.9 |
| `lint_design` warning | `spacing-grid` (off the 4px grid) | Designer | 6.9 |
| `lint_design` warning | `touch-target` (under 44px) | Accessibility | 6.9 |
| `lint_design` warning | `layer-name` (default names) | Designer | 6.9 |
| `lint_design` warning | `hard-coded-color` | Brand | 6.9 |
| `lint_design` info | `type-scale` | Designer | none — must be listed |
| `verify_code` error | `missing-text` | Copy | 3.9 |
| `verify_code` error | `hardcoded-color` | Brand | 3.9 |
| `verify_code` error | `forbidden-absolute` | Designer | 3.9 |
| `verify_code` error | `missing-layout` | Designer | 3.9 |
| `verify_code` warning | `missing-component` | Brand | 6.9 |
| `verify_code` warning | `spacing-drift` | Designer | 6.9 |
An error caps the owning panel at 3.9 (Blocker band), a warning at 6.9 (Weak band), an `info` item caps nothing but must appear in the panel's notes. `hard-coded-color` arrives in both flavours: a warning when the color equals a variable that exists, an info when it is a literal used several times — the warning caps Brand, the info does not. The cap lifts as soon as the finding is gone; the next round re-scores the panel freely.
A finding is evidence for its owner and a hint for the others: `contrast` also informs the Critic, `spacing-grid` also informs the Brand (was it the token or the layout?), but only the owner is capped.
## 5. The panels and the composite
| Panel | Judges | Weight |
| Persona | Asks | Method |
|---|---|---|
| Designer | layout, composition, hierarchy, spacing rhythm, type scale in use | **0 in v1** — it still scores and its findings still drive the next round |
| Critic | does it meet the brief; contrast, weight, readability | 0.4 |
| Brand | token compliance, on-brand color, voice | 0.2 |
| Accessibility | WCAG, focus rings, semantic structure, alt text | 0.2 |
| Copy | voice, tone, terseness, error-message quality | 0.2 |
| `vision` | Does it achieve the objective, as the person imagined it? | Objective, element, effect, why (Connor and Irizarry) |
| `ux` | Can people use it without help? | Nielsen's 10 heuristics |
| `flow` | Do the screens make one coherent journey per task? | Task walkthrough across screens, 4 questions per step |
| `qa` | What breaks with real data and states? | State and edge-case matrix, pass/fail/n.a. |
| `visual` | Does the eye land right, does it hold together? | Hierarchy, balance, contrast, gestalt, rhythm, tokens |
| `accessibility` | Can everyone use it? | W3C POUR applied to a design |
| `copy` | Is every word clear and specific? | Plain language, labels, errors, empty states |
| `end-user` | Where would this user get stuck or give up? | Persona walkthrough of the top task |
```
composite = critic x 0.4 + brand x 0.2 + accessibility x 0.2 + copy x 0.2
```
`vision` always runs: the person's intent is the yardstick for the others. Pick the rest with
`personas` (default: all). The person's own words outrank any reviewer.
The weights sum to 1.0, so the composite is also 0–10. The Designer carries weight 0 in v1 on purpose: it is the panel most likely to argue with itself, so its score is reported and its findings are acted on, but it does not move the composite.
## 4. Independence
Each panel scores 0–10 in half-point steps. The composite is the weighted sum, rounded to two decimals.
Several reviewers find far more than one, but only when they work apart (Nielsen: one
evaluator finds about a third of the problems, three to five most of them). Reviewers who
discuss or debate converge and amplify each other's bias.
| Composite | Verdict |
* **With subagents**: one subagent per persona, given only its persona brief, the objective and
the evidence. No reviewer sees another's findings before rating.
* **Alone**: one pass per persona, in order, writing that persona's findings before reading the
next brief. Do not revise earlier personas' findings during later passes.
* **You coordinate**: you merge and judge; you never let reviewers argue a finding out.
## 5. Evidence
From `critique_design`: lint (fix every error and warning first), the `flow` graph (arrows between
screens, `deadEnds`, `unreached`), the lessons. Then `get_screenshot` each artboard at scale 1
(2 for small text) in every variable mode, `get_bounds` for overflow, `get_feedback` for the
person's notes. Every finding cites one of these: a tool result or a screenshot region. No
evidence, no finding.
## 6. A finding
`persona`, `heuristic or check`, `element` (layer name), `seen` (the observation), `effect` (on
the objective), `evidence`, `severity`, `suggestion`. Keep the observation and the suggestion
apart: critique is analysis, not brainstorming. Each persona also names at least one thing that
works and should stay.
Severity (Nielsen), from frequency, impact and persistence:
| | |
|---|---|
| ≥ 8.0 | Ship |
| < 8.0 | Not shippable as is: fix and re-run, or ship the best round after round 3 |
| 0 | not a problem |
| 1 | cosmetic: fix if time allows |
| 2 | minor: low priority |
| 3 | major: fix before handing over |
| 4 | catastrophe: blocks the task, fix now |
The band legend is the same for every panel:
Every broken lesson (`lessons`) is a finding of at least 3.
| Band | Range | Means |
|---|---|---|
| Blocker | 0.0–2.9 | Broken: the panel's own evidence is violated. Cannot ship |
| Poor | 3.0–4.9 | Works against the brief or the brand; ships visible damage |
| Weak | 5.0–6.9 | Passable but noticeably off; expect a second round |
| Acceptable | 7.0–7.9 | No blocker, but a reviewer would ask for one change |
| Good | 8.0–9.4 | Ships as is |
| Excellent | 9.5–10.0 | Reference quality for this project |
## 7. Merge, fix, report
What each panel inspects, which tool backs it and where its scores land: [panels.md](references/panels.md).
1. **Merge** into one list: join duplicates, count the personas that found each (two or more is
confirmed), severity is the mean of their independent ratings.
2. **Fix** every 3 and 4 with the smallest edit (`update_styles`, `set_text_content`), then check
that finding again with the same evidence. The work is ready when no 3 or 4 is open and lint is
clean. Three fix rounds at most; then hand over with the open findings stated.
3. **Report** to the person: what works, then findings by severity, each with what was seen, the
evidence, the suggestion and whether it was fixed. Phrase open ones as questions about the
objective ("How does a first-time user find Reorder from here?"), not verdicts.
## 6. Rounds and the fallback
## 8. Lessons
* At most **3 rounds** per gate. The counter is per gate, not per panel.
* A round ends when the composite is computed.
* Rounds 2 and 3 exist to fix what the panels found. Fix in composite-impact order: the panel with the largest `weight x (10 - score)` first.
* **`fallbackPolicy: ship_best`.** If the third round is still below 8.0, the status is `Below threshold` and what ships is the best round, not the last one. Best = highest composite; a tie goes to the earlier round (fewer changes). A later round edited the canvas, so put the canvas back into the best round's state before handing over — undo back to it, or re-apply that round's fix list in order.
* Ship the best round's artifact and report the failing score. `Below threshold` is a status, not a licence to hide the number.
Ask the person with `ask_user` which findings to remember, one question per finding: for every
project, for this project, or not. Save only what they approve, with
`design_lessons {add: {rule, why, scope}}`: the rule is one line written as an instruction
("Screens keep their device height; the main column scrolls"), `why` is what went wrong. Never
save a lesson the person did not approve, and never save praise. Lessons come back in
`get_basic_info` and in the next `critique_design`; `design_lessons {remove: "L3"}` drops one.
## 7. Status vocabulary
## Sources
Every gate ends with exactly one status.
| Status | When | What ships |
|---|---|---|
| `Shipped` | composite ≥ 8.0 | the reviewed artifact, unchanged |
| `Below threshold` | still < 8.0 after round 3 | the best-scoring round's artifact (`ship_best`), with the score reported |
| `Timed out` | the gate ran out of its budget (rounds or minutes) before a verdict | the best round so far, with the timeout stated |
| `Interrupted` | the user or the session stopped the gate mid-run | the last complete round's artifact |
| `Degraded` | a panel could not be judged properly (section 8) | the artifact, but the verdict is provisional and the reason is named |
`Degraded` outranks `Shipped`: a degraded panel is capped at 6.9, so a degraded run reports `Degraded`, not `Shipped`, even when the composite clears 8.0.
## 8. Degraded reasons
A panel that cannot be judged on real evidence is not scored as if it were fine. It is capped at 6.9, the composite is computed with the cap, and the run reports `Degraded` with the reason and the panel it hit.
| Reason | Cause | Handling |
|---|---|---|
| `malformed_block` | a tool returned something the gate could not parse (lint payload in an unexpected shape, a truncated screenshot answer) | retry the tool once; if it still fails, judge the panel on the evidence you do have and name the missing part |
| `oversize_block` | the evidence was too big to read whole (a screenshot scaled down past legibility, a tree deeper than the context, a lint payload whose `truncated` flag is set) | narrow the scope: screenshot or lint a subtree, split the screen into regions and judge region by region |
| `adapter_unsupported` | the requested target or format is not supported here (a `verify_code` target the project does not build) | drop that check; the panel falls back to the screenshot plus the design rules |
| `protocol_version_mismatch` | the MCP server exposes a different tool set than the gate expects (a tool renamed, an argument changed) | stop using the broken tool, finish what you can, report `Degraded`; do not guess the schema |
| `missing_artifact` | the thing under review is gone (artboard deleted, page switched, stale node id) | re-snapshot with `get_basic_info` and restart the gate; if the artifact is truly gone, report `Degraded` |
Degradation is a report, not an excuse: name the reason, the panel and the evidence you lost.
## 9. What the gate reports
End the gate with the status, the numbers and the findings, in that order:
```
Critique gate - Sign in (page p1, artboard 3fa9c1:7)
Round 2 of 3 - composite 8.44 / threshold 8.00 - status Shipped
Designer 7.5 (weight 0) | Critic 8.5 | Brand 8.5 | Accessibility 8.5 | Copy 8.2
Composite = 8.5x0.4 + 8.5x0.2 + 8.5x0.2 + 8.2x0.2 = 8.44
```
Then, per finding: the panel, what was seen, the evidence behind it (tool and finding, or the region of the screenshot), the fix, and whether it was fixed in this round or is still open.
Machine-readable form, when the caller wants JSON:
```json
{
"status": "Shipped",
"threshold": 8.0,
"composite": 8.44,
"round": 2,
"rounds": [{ "round": 1, "composite": 7.35 }, { "round": 2, "composite": 8.44 }],
"scores": { "designer": 7.5, "critic": 8.5, "brand": 8.5, "accessibility": 8.5, "copy": 8.2 },
"weights": { "designer": 0.0, "critic": 0.4, "brand": 0.2, "accessibility": 0.2, "copy": 0.2 },
"degraded": [],
"findings": [{ "panel": "critic", "evidence": "get_screenshot: headline at 16px over a photo", "severity": "warning", "state": "fixed" }]
}
```
## 10. Attribution
The five-panel critique contract — the roles, the weights, the composite, the 8.0 threshold, the three rounds and the `ship_best` fallback — is inspired by OpenDesign's Critique Theater (https://github.com/nexu-io/open-design, Apache-2.0). The contract is re-implemented here in PaperCraft's own words and against PaperCraft's own tools; no text was copied.
Adam Connor and Aaron Irizarry, *Discussing Design* (objective, element, effect, rationale);
Nielsen Norman Group: 10 usability heuristics, severity ratings, cognitive walkthroughs, design
critiques, visual design principles; W3C WAI accessibility principles (POUR); work on panels of
independent LLM judges (debate amplifies bias, a coordinating judge resists it).
@@ -1,182 +0,0 @@
# Panel rubrics
Five panels, one artifact, one set of evidence. A panel is a pass with a fixed rubric: what it inspects, which tool output backs it, what it does **not** judge, and the band each finding lands in. The panels share the band legend in [../SKILL.md](../SKILL.md) (Blocker 0–2.9, Poor 3–4.9, Weak 5–6.9, Acceptable 7–7.9, Good 8–9.4, Excellent 9.5–10).
## How to run one panel
1. Read the evidence you were handed: the lint payload, the screenshot, the design spec, the variables, the code result.
2. Write findings. Every finding cites its evidence — the lint rule and node, the region of the screenshot, the spec's MUST rule, the `verify_code` violation.
3. Score the panel 0–10. The score is the panel's verdict on the whole screen, not an average of its findings: two serious defects on one region cost more than two trivial ones on two regions, but a screen with three medium defects is not "Good" either.
4. Apply the caps. Hard evidence caps the owning panel (SKILL.md section 4); degraded evidence caps it at 6.9 (SKILL.md section 8).
Evidence a panel may not use: taste, guesses about a user's preferences, or a claim about something no tool showed. When a panel wants to judge what no tool exposes — whether an image is meaningful, whether a flow makes sense — it raises the question as an open item instead of scoring it as a defect.
Tool arguments used below are the current names. Note that `lint_design`'s `scale` is the list of allowed font sizes, not a zoom; the zoom argument of `get_screenshot` is `scale: 1 | 2`.
---
## Designer — weight 0 in v1
**Judges:** layout and composition, alignment, spacing rhythm, hierarchy, the type scale in use, how things are grouped, screen states, overflow, density.
**Evidence it uses**
| Tool | How |
|---|---|
| `get_screenshot` (`nodeId`, `scale: 2`) | the primary evidence: alignment, edges, rhythm, hierarchy, clipping, empty areas |
| `lint_design` (`nodeId`, `pageId`) | `spacing-grid`, `type-scale`, `layer-name` findings |
| `get_design_spec` (`nodeId`, `target`) | flow and sizing (`fixed`, `hug`, `fill`), grouping, where the spacing rules were broken |
| `get_tree_summary` (`nodeId`, `depth`) | structure depth, unnamed frames, accidental nesting |
**Does not judge:** contrast and WCAG (Accessibility), token and palette choices (Brand), wording (Copy).
**Bands**
| Band | What the Designer saw |
|---|---|
| 0.0–2.9 | Broken layout: frames overlap, content sits outside the artboard, text is clipped, the screen reads as a pile |
| 3.0–4.9 | Poor composition: everything on the default grid, equal weight everywhere, no grouping, ragged edges |
| 5.0–6.9 | Weak: spacing off the 4px grid in places, sizes off the type scale, mixed grouping (space *and* containers everywhere), a state missing |
| 7.0–7.9 | Acceptable: coherent, but one region is crowded or one rhythm is broken |
| 8.0–9.4 | Good: clear hierarchy, aligned to a few strong edges, consistent rhythm, states present |
| 9.5–10.0 | Excellent: deliberate rhythm, one memorable idea, whitespace used as a material |
**Fixes that move the score:** snap spacing to the 4/8 scale (`lint_design` with `fix: true`), pull one size step per level, give each group either a container or space — not both, align to one left edge, add the missing empty/loading/error state.
---
## Critic — weight 0.4
**Judges:** whether the screen meets the brief (the request this run is executing), whether visual weight matches the priority, readability as a human experiences it, and whether the real cases are covered.
**Evidence it uses**
| Tool | How |
|---|---|
| the brief | the user's request and the `papercraft-design` section 0 direction, read against the screenshot |
| `get_screenshot` (`nodeId`, `scale: 2`) | one primary action, reading order, what is above the fold, legibility by eye |
| `lint_design` (`nodeId`, `pageId`) | `contrast`, `text-size`, `type-scale` findings as the objective part of readability |
| `get_design_spec` (`nodeId`, `target`) | what the screen actually contains, against what was asked for |
| `verify_code` (`nodeId`, `target`, `code`) | when the design is code-bound: whether the implementation kept the intent |
**Does not judge:** which palette (Brand), line-level wording (Copy), implementation details beyond fidelity to the design.
**Bands**
| Band | What the Critic saw |
|---|---|
| 0.0–2.9 | Misses the brief: wrong screen or wrong content, the primary action is absent or ambiguous |
| 3.0–4.9 | Fights the brief: right elements, wrong order or emphasis, or hard to read (contrast errors, tiny type) |
| 5.0–6.9 | Weak: it does the job, but the weight is flat and something important sits below the fold or is buried |
| 7.0–7.9 | Acceptable: meets the brief; one area stays unclear or the priority is not obvious |
| 8.0–9.4 | Good: the brief is met, the primary action is obvious, readability is solid |
| 9.5–10.0 | Excellent: unmistakable hierarchy, the screen reads in one pass, the edge cases are covered |
**Fixes that move the score:** make one thing clearly primary and quiet the rest, move the important content up, cut a competing element, re-read the brief line by line and check each requirement against the screenshot.
---
## Brand — weight 0.2
**Judges:** token compliance, on-brand color, semantic color use, mode coverage, product voice consistency, component reuse.
**Evidence it uses**
| Tool | How |
|---|---|
| `get_variables` (`pageId`, `format`) | the variables, tokens, active `mode` and available `modes` — the reference palette |
| `lint_design` (`nodeId`, `pageId`) | `hard-coded-color` findings |
| `get_computed_styles` (`nodeIds`) / `find_nodes` (`filters`) | fills and text colors that are not following a variable |
| `get_design_spec` (`nodeId`, `target`) | which tokens the design already binds |
| `get_components` / `get_tree_summary` | components built and instanced versus copies |
| `verify_code` (`nodeId`, `target`, `code`) | `hardcoded-color`, `missing-component` in the implementation |
**Does not judge:** composition (Designer), wording line by line (Copy), whether the layout serves the brief (Critic).
**Bands**
| Band | What the Brand saw |
|---|---|
| 0.0–2.9 | Off-system: hard-coded colors where tokens belong, a foreign palette, no modes |
| 3.0–4.9 | Off-brand: technically on-token but the accent is used everywhere, or the screens read as different products |
| 5.0–6.9 | Weak: one or two hard-coded colors, the accent covers much of the screen, components copied instead of instanced |
| 7.0–7.9 | Acceptable: on-system, with one token or component inconsistency |
| 8.0–9.4 | Good: every color is a token, the accent marks the primary action only, modes defined and checked, components reused |
| 9.5–10.0 | Excellent: coherent across screens and modes, the palette carries meaning |
**Fixes that move the score:** `lint_design` with `fix: true` relinks hard-coded colors that match a variable, add the missing variable before using the color, cut accent back to the primary action, build the repeated part once and place instances, check the second mode with `set_variable_mode`.
---
## Accessibility — weight 0.2
**Judges:** WCAG contrast, the text-size floor, touch-target size, focus and hover states, semantic structure, alt text, and signalling that does not rely on color alone.
**Evidence it uses**
| Tool | How |
|---|---|
| `lint_design` (`nodeId`, `pageId`) | `contrast`, `text-size`, `touch-target` findings |
| `get_screenshot` (`nodeId`, `scale: 2`) | whether a visible focus or hover state was designed at all, and how the error state reads |
| `get_design_spec` (`nodeId`, `target`) | which text is a title, how the screen is grouped, what each control is |
| `find_nodes` (`textValue`, `filters`) | image and icon nodes, unlabeled icon buttons |
| `get_tree_summary` (`nodeId`, `depth`) | semantic structure: heading levels, groups, control order |
**Does not judge:** visual taste (Designer), palette taste (Brand), voice (Copy).
**Bands**
| Band | What Accessibility saw |
|---|---|
| 0.0–2.9 | Fails WCAG: contrast errors, text under 12px, targets under 44px, meaning carried by color alone |
| 3.0–4.9 | Poor: borderline contrast, unlabeled controls, an error state that is red text and nothing else |
| 5.0–6.9 | Weak: passes on the desktop artboard but fails on the mobile one, one image without alt text |
| 7.0–7.9 | Acceptable: compliant baseline, one gap (no visible focus state, one icon-only button) |
| 8.0–9.4 | Good: WCAG clean, targets pass, semantics and labels present, focus and hover states designed |
| 9.5–10.0 | Excellent: every state specified, sensible keyboard order, no meaning by color alone |
**Fixes that move the score:** fix the color, not the check, until `lint_design` reports no contrast error, name interactive layers `Button…` / `Tab /…` so the target rule can see them and grow them to 44px, pair every error color with an icon or a word, design the focus and hover variants as components, give every image an alt text.
PaperCraft has no focus-ring or alt-text checker: judge those from the design itself (a `Button / Focused` variant, alt text in the spec) and report what is missing as a gap, not as a tool finding.
---
## Copy — weight 0.2
**Judges:** voice and tone against the product's voice, terseness, labels and microcopy, error-message quality, empty states, placeholders versus labels, the absence of filler.
**Evidence it uses**
| Tool | How |
|---|---|
| `get_design_spec` (`nodeId`, `target`) | the authoritative copy: every text node's content, in order |
| `get_screenshot` (`nodeId`, `scale: 2`) | how the copy reads in context: line breaks, truncation, all-caps abuse |
| `find_nodes` (`textValue`) | repeated strings, leftover placeholders, "Lorem ipsum", inconsistent labels for the same action |
| `verify_code` (`nodeId`, `target`, `code`) | `missing-text` — copy that was lost between the design and the code |
**Does not judge:** typography and layout (Designer), color (Brand), whether the screen meets the brief (Critic).
**Bands**
| Band | What Copy saw |
|---|---|
| 0.0–2.9 | Wrong: placeholder text, lorem ipsum, or copy that contradicts the brief |
| 3.0–4.9 | Poor: filler adjectives, two voices on one screen, errors that blame the user |
| 5.0–6.9 | Weak: wordy, mixed formality, vague labels ("Submit" where "Save changes" is meant) |
| 7.0–7.9 | Acceptable: clear but plain; one error message says only "Error" |
| 8.0–9.4 | Good: terse, one voice, every action labeled with a verb, errors say what happened and what to do next |
| 9.5–10.0 | Excellent: reads like a product that shipped, not like a mockup |
**Fixes that move the score:** cut every word that does not inform, give each action a verb label ("Save changes", not "OK"), write errors as *what happened + what to do next*, fill empty states with an explanation and the next action, replace placeholder copy with real content, keep one formality level per screen.
PaperCraft has no spell-checker or copy linter: the copy panel reads the spec and the screenshot, and it flags what a human reviewer would flag — it does not invent grammar rules.
---
## Scoring discipline
* Half-point steps only: 7.5, 8.0, 8.5. Never 8.37.
* The score is a verdict on the screen, not an average of findings.
* A panel that finds nothing wrong and cites no evidence scores 7.0–7.9, not 10. A 10 means the screen is a reference for the next one.
* Caps bind: hard evidence (SKILL.md section 4) and degraded evidence (section 8) are applied after the verdict, never negotiated with.
* No panel may raise another panel's score. If the Critic likes the hierarchy, that stays a Designer finding.
@@ -0,0 +1,110 @@
# Reviewer checklists
Each reviewer works from its own list, alone, and cites evidence for every finding. Mark each
item pass, fail or n/a; a state or screen that was never designed is an open question, not a pass.
## vision: the person's vision
For each key element on each screen, four questions (Connor and Irizarry):
1. **Objective**: which part of the objective is this element for?
2. **Element**: which elements serve it?
3. **Effect**: do they achieve it, for this audience?
4. **Why** or why not?
Also: everything the brief, references and feedback notes asked for is there; nothing contradicts
an open or resolved note (`get_feedback`); the tone matches the references. Unknown product facts
are questions for the person, never guesses.
## ux: usability heuristics (Nielsen)
1. Visibility of system status: progress, saving, selection shown at once.
2. Match with the real world: the users' words and order, not the system's.
3. User control and freedom: a marked exit, undo, cancel.
4. Consistency and standards: one word and one look per thing, platform conventions.
5. Error prevention: risky actions confirmed, impossible choices removed.
6. Recognition rather than recall: options visible, no remembering across screens.
7. Flexibility and efficiency: shortcuts for experts that novices need not see.
8. Aesthetic and minimalist design: nothing that competes with what matters.
9. Recognize, diagnose, recover from errors: plain language, the problem, the fix.
10. Help and documentation: needed rarely, and then where the task is.
## flow: coherent journeys
Walk each top task across screens (the `flow` graph when arrows connect screens, else the screen
order and names; ask the person when the intended order is unclear). At each step, the four
walkthrough questions: will they try to do this, will they notice the control, will they connect
it to their goal, will they see progress afterwards?
* Every task has an entry point (including first run and empty state) and a clear end
(success screen or state).
* Every screen has a way forward and a way back or out; no dead ends (`deadEnds`), no screen
nothing leads to (`unreached`) unless it is an entry.
* Each action that can fail has its error state; each destructive one a confirmation and undo.
* Navigation, back and primary actions sit in the same place with the same labels on every screen.
* What the user entered or chose shows on the following screens (the cart count, the name typed).
* Steps a user must remember between screens are shown instead (recognition over recall).
* The number of steps fits the objective ("two taps" means two).
## qa: states and edge cases
Per screen and component, pass, fail or n/a:
* States: default, hover, focus, pressed, disabled, selected, loading, empty, error, success.
* Content: the longest realistic text and names, minimum content, zero, one and many items,
missing images and avatars, large numbers, special characters.
* Modes and sizes: every variable mode (screenshot each); the narrow and wide screen sizes when the
design has them.
* Layout: nothing overflows or clips (`get_bounds`, lint `overflow`), screens keep their device
height and scroll inside, fixed bars stay fixed.
* Consistency: repeated pieces are instances of the same component (lint `not-an-instance`).
## visual
* One focal point per screen; the primary action is the most prominent control.
* Scale and hierarchy: size, weight and colour follow importance.
* Balance and alignment: shared edges, a steady grid.
* Gestalt proximity: related items closer together than unrelated ones.
* Rhythm: spacing from the 4px scale, the same gaps for the same relationships.
* Type: sizes on the scale, at most three levels per screen.
* Tokens: every colour a variable, consistent radii, components reused.
## accessibility (W3C POUR, for a design)
**Perceivable**
* Text contrast 4.5:1, 3:1 for large text (18px, or 14px bold) and for control borders and icons.
* Colour is never the only signal (errors also have an icon or text; links are underlined or bold).
* Text is real text, not pictures of text; icons that act have a label or tooltip.
* The layout holds at 320px wide and with text at 200% (a narrow artboard shows it).
**Operable**
* Touch targets 44px (24px minimum with spacing).
* A visible focus state is designed for every interactive component.
* Headings, a page title and a visible current location (active nav item).
* Destructive actions confirmed or undoable; every gesture has a button alternative.
* Nothing flashes; motion can be reduced.
**Understandable**
* Fields keep a visible label (a placeholder alone disappears).
* Errors say what went wrong and how to fix it, next to the field.
* Navigation and labels are the same across screens.
* Plain language; abbreviations explained.
**Robust**
* Controls are named for their role (layer names, code links to real components), so the code
hand-off keeps them buttons and inputs, not divs.
## copy
* The users' words, not internal jargon; verbs on buttons ("Save changes", not "OK").
* No placeholder or lorem text, no "Item 1".
* Errors: what happened and what to do. Empty states: what this is and the first action.
* Neutral wording about people whose gender is unknown (their name, or they).
* Consistent terms: one word per concept across all screens.
## end-user: a walkthrough as the audience
Take the `audience` (else ask). Write as that person, in the first person, for each top task:
what I see first, what I think it does, what I try, where I hesitate, and the first point where I
would give up. One persona per run; launch another critique with a different `audience` for a
second user type.
+2 -2
View File
@@ -33,7 +33,7 @@ Allowed spacing values: **4, 8, 12, 16, 24, 32, 48, 64** (and 96 for page sectio
## 2. Tokens first (variables)
* All colors are variables (`var(--name)`), named by role: `bg`, `surface`, `border`, `text`, `text-muted`, `accent`, `on-accent`, `success`, `warning`, `danger`. Two to three surface levels at most (`bg` < `surface` < `surface-2`).
* Define at least two modes if the product may be dark or light (`set_variables` with `mode`), and check both: lint contrast in each mode after `set_variable_mode`.
* Define at least two modes if the product may be dark or light (`set_variables` with `mode`), and check both: lint contrast in each mode after `set_variable_mode`, or look with `get_screenshot {mode}` without switching. Themes are modes, never copies: one artboard per screen, never a light and a dark duplicate (`lint_design` flags them as `theme-copy`).
* Semantic colors carry meaning only: red means error or destructive, green means success. Do not use them decoratively.
* If a color you need is not a variable, add the variable, then use it. `lint_design` reports hard-coded colors that equal a variable.
* Spacing, radius and type sizes are number variables (`space/md`, `radius/sm`, `font/body`): define them once, in a collection of their own, and bind fields with `bind_variable`. Keep the values on the lists in this skill. Group names with `/` and alias semantic names to base ones (`color/button/bg` to `color/brand/primary`).
@@ -105,7 +105,7 @@ Run it before `finish_working_on_nodes`:
1. Direction written down; the screen has one memorable idea and one primary action.
2. Every color is a variable; both modes checked if there are two.
3. Spacing only 4, 8, 12, 16, 24, 32, 48, 64, 96; type only from the scale; radii from one family.
4. `lint_design` has zero errors and zero warnings, and the `info` items were considered.
4. A full `lint_design` (no arguments) has zero errors and zero warnings, and the `info` items were considered. Between edits, lint only what changed: see `papercraft/references/incremental-checks.md`.
5. Screenshot at scale 2 looked at: hierarchy, alignment, wrapping, clipping, empty areas, contrast by eye.
6. Interactive things at least 44px on touch and named `Button…` / `Tab /…`.
7. Repeated parts are components; states exist for every interactive component and for empty, loading and error screens.
+21 -9
View File
@@ -17,7 +17,7 @@ papercraft-cli call get_basic_info # fails with exit 1 if the app is not ru
* **Check the project is the right one.** `get_basic_info` returns `fileName` and `path`. Build only in a project that belongs to the work (a NotesCraft design file is for NotesCraft screens, not for redesigning PaperCraft). For a different product: `create_file {name}`, then `open_file {fileId}`, and say so. `open_file` swaps the project the user is looking at, so tell them. Pass `fileId` on later calls when more than one project is in play.
* Not running: ask the user to start PaperCraft and open a project (or `create_file` then `open_file`). `PAPERCRAFT_MCP_PORT` must match if the port was changed. Do not try to launch the GUI yourself unless asked.
* Read `get_basic_info` (pages, artboards, sizes, fonts), then `get_selection` (what the user is looking at) and `get_variables` and `get_components`. Reuse what exists before creating anything.
* Read `get_basic_info` (pages, artboards, sizes, fonts, and `lessons`: the person's rules from past critiques; follow every one), then `get_selection` (what the user is looking at) and `get_variables` and `get_components`. Reuse what exists before creating anything.
* `pageId` defaults to the page the user is viewing, which can change between calls: pass it explicitly. Node ids look like `3fa9c1:14`; never show them to the user.
* App closed (server, CI)? `papercraft-cli call --offline ... --project <name>` runs the same tools on the project store and saves after every editing call; `papercraft-cli shell` keeps one session for many calls and reads scripts from stdin. It is refused while the app has that project open. See `docs/agents.md`.
* With MCP tools you do not need the CLI; with a shell, `papercraft-cli list` and `papercraft-cli schema <tool>` give the schemas on demand. Pass big JSON on stdin: `... | papercraft-cli call write_html -`.
@@ -33,8 +33,10 @@ papercraft-cli call get_basic_info # fails with exit 1 if the app is not ru
3. **Build in small steps.** `create_artboard`, then one `write_html` per visual group (header, card, row) so the user sees progress. Name every layer with `layer-name`.
4. **Look.** `get_screenshot` of the artboard after each group or two. Scale 2 to read small text. Read it as a designer: hierarchy, alignment, spacing rhythm, clipping, wrapping.
5. **Lint.** `lint_design` on the artboard. Fix every error and warning; consider the `info` items. Run it again until errors and warnings are zero.
Between edits, lint only what changed: keep the `rev` each write returns and run `lint_design {changedOnly: true}` (or `{since: rev}`). `get_changes {since}` shows what an edit touched; `get_bounds` measures a whole subtree in one call instead of looping `get_node_info`.
Always finish with one full `lint_design` (no `since` or `changedOnly`: `covered.partial` is false) and a screenshot per artboard in each theme mode before reporting. Details: [incremental-checks.md](references/incremental-checks.md).
6. **Fix with the smallest edit.** `update_styles` and `set_text_content` on the existing nodes, not a rewrite. Rewrite a group with `write_html` mode `replace` only when its structure is wrong.
7. **Finish.** `finish_working_on_nodes` (saves). Tell the user what you made in a sentence or two, with artboard names, not ids.
7. **Finish.** `finish_working_on_nodes` (saves). Tell the user what you made in a sentence or two, with artboard names, not ids. When its result has `next`, offer a critique with `ask_user`; on yes run `critique_design` and follow its steps (the `papercraft-critique` skill has the full gate). Keep a finding as a lesson (`design_lessons {add}`) only when the person approved it.
Lint passes are necessary, not sufficient: contrast and grids say nothing about composition. The screenshot is the real test.
@@ -44,8 +46,12 @@ Lessons from redesigning a 3-page, 130-screen project with up to six agents at o
* **Split by section, not by page.** Give each agent named sections (frame ids) and forbid the rest. New sections go below the current lowest one; re-read positions right before creating, since another agent may have just added one.
* **Section layout**: a section frame holds a title and rows of screen slots (a route label above a fixed-size screen). Keep 640 px between screens and rows and 1600 px between sections, so notes fit beside screens. Stack sections in one column.
* **See what the others changed** before building on their work: `get_changes {since: <your last rev>, exclude: "self"}` lists layers by the client that changed them. `truncated: true` means re-read everything.
* **Name yourself and fence your area.** With the CLI pass `--as <name>` (or `PAPERCRAFT_AGENT`), so writes, conflicts and claims carry your name. `claim_nodes {ids, note}` the sections you will rewrite; others' writes there are refused until you `finish_working_on_nodes` or `release_claims`. A denial names the holder: work elsewhere, never wait in a loop.
* **Writes report conflicts.** A result with a `swarm` block lists layers another agent changed since you last looked: read them with `get_changes` before going on. Pass `ifRev: <your last rev>` to have the write refused instead (`"error": "conflict"`).
* **Undo takes back only your own steps** (`undo`, scope `mine`); it is refused with `undo_blocked` when someone changed the same layers since. Fix forward then; `scope: "page"` undoes whoever was last.
* **Recreate, then delete.** Build the new screen next to the old one, finish it, then delete the old one, and record the deleted ids and names in the report. A cleanup agent once deleted buttons and lost icons by editing in place.
* **Reuse the design system across pages**: `create_instance` / `swap_instance` / `<x-paper-instance>` take a main from any page and stay linked. Never `import_components` to use a component: that copies mains onto the page, and instances made from the copies stop following the design system. `lint_design` flags hand-made look-alikes as `not-an-instance` (with the component key to use).
* **Reuse the design system across pages**: `create_instance` / `swap_instance` / `<x-paper-instance>` take a main from any page and stay linked. Never `import_components` to use a component: that copies mains onto the page, and instances made from the copies stop following the design system. `lint_design` flags hand-made look-alikes as `not-an-instance` (a different icon is a different component); fix with `write_html` mode replace and the `<x-paper-instance>` it names.
* **QA pass after a build** (screen by screen at scale 1, one call at a time): text layers with stale fixed widths clip labels ("In prog", "New sess"): set `width: fit-content`. Sidebars and panels fixed at 900 px stop short on taller screens: make them fill the height and let the screen hug with a 900 px minimum. Tags and chips overflowing their box, cards overlapping buttons, footers not pinned, leftover `Text` / `Frame` names, gendered copy about people whose gender is unknown (use their name or neutral wording).
* **Report per agent** in `specs/<feature>/<agent>-report.md`: what changed (ids), why (playbook rule ids from `playbook search`), open questions. Unknown product facts are written as questions for the team, never invented.
@@ -89,7 +95,7 @@ For a screen with dozens of parts, drive the CLI from a short Python helper (`ca
```
Artboards: mobile 390×844, tablet 768×1024, desktop 1440×900, large desktop 2560×1440 (a 1440p display; ask which the user uses and design that first). A narrow window (1280×800) is its own artboard: collapse the sidebar to an icon rail, make secondary actions icon-only with a tooltip, keep one labelled primary action.
Dot-grid canvas (no pattern fill exists): one `<svg>` with a single path of tiny round-capped dashes, absolute at 0,0 inside the canvas frame: `<svg style="position:absolute;left:0px;top:0px" width="W" height="H" viewBox="0 0 W H"><path d="M16 16h0.01M48 16h0.01..." stroke="var(--surface-2)" stroke-width="2.5" stroke-linecap="round" fill="none"/></svg>`. One vector layer, about 3,000 dots at a 32px step. Its colour is frozen at write time. Create with `height:"fit-content"` when the content decides the height. Replace margin with `gap` and `padding`.
Dot-grid canvas (no pattern fill exists): one `<svg>` with a single path of tiny round-capped dashes, absolute at 0,0 inside the canvas frame: `<svg style="position:absolute;left:0px;top:0px" width="W" height="H" viewBox="0 0 W H"><path d="M16 16h0.01M48 16h0.01..." stroke="var(--surface-2)" stroke-width="2.5" stroke-linecap="round" fill="none"/></svg>`. One vector layer, about 3,000 dots at a 32px step. Its colour is frozen at write time. A screen keeps its device's size; at a device width (390, 1440...) `height:"fit-content"` becomes the device height. Content taller than the screen scrolls by itself: the main column under a fixed sidebar and header, else the screen (write_html says which). Pass `overflow:"visible"` to let a board grow (a landing page drawn in full). Replace margin with `gap` and `padding`.
Full flows: [recipes.md](references/recipes.md).
@@ -100,9 +106,11 @@ Full flows: [recipes.md](references/recipes.md).
| Know the project, pages, artboards | `get_basic_info`, `get_tree_summary`, `get_children` |
| Know what is selected | `get_selection` |
| Inspect one node | `get_node_info`, `get_computed_styles`, `get_jsx` |
| Sizes and positions of a subtree | `get_bounds` (one call, `columns` + `rows`; `continueWith` when `truncated`) |
| What changed since a revision | `get_changes {since}` (`exclude: "self"` for other agents' edits) |
| Find nodes by text or style | `find_nodes` (text contains the phrase, any case, every page) |
| Read all copy in a subtree | `get_tree_summary {nodeId, fullText: true}` (depth counts from the node: rows under a "Screens" frame need depth 3) |
| See it | `get_screenshot` (`nodeId`, `scale` 1 or 2, `annotations: true` to include notes) |
| See it | `get_screenshot` (`nodeId`, `scale` 1 or 2, `annotations: true` to include notes, `mode` to preview a theme without switching) |
| Show the user something | `set_viewport` (page switch, fit, zoom) |
| Document rules on the design | `annotate`, `get_annotation_template`, `get_annotations`, `update_annotation`, `delete_annotations` |
| New screen | `create_artboard` (`width` and `height` required) |
@@ -112,18 +120,22 @@ Full flows: [recipes.md](references/recipes.md).
| Copy, move, delete | `duplicate_nodes`, `move_nodes`, `delete_nodes` |
| Tokens | `get_variables` (`format` css, json or tailwind), `set_variables` (`collection`, `type`, `mode`, `alias`), `create_collection`, `delete_variables`, `set_variable_mode`, `bind_variable` |
| Reuse | `create_component`, `create_instance`, `swap_instance` (also a nested badge inside a row instance: a swap that survives main edits), `get_components`, `<x-paper-instance>` |
| Component properties | One component with properties instead of one per state: `set_component_props` (`text`, `bool`, `icon`, `enum` options in the overrides format; templates `text`, `active` for tab bars and navs, `status` with [{value, color}]), then `set_instance_props {nodeIds, values}` or `<x-paper-instance component="Badge" props='{"Status":"Rejeitada"}'/>`; `get_components` lists them |
| Merge per-state components | `merge_variants {componentIds, name}` turns `Tab bar / Hoje`, `Tab bar / Lojas`... into one `Tab bar` with an enum, moves every instance and deletes the emptied variants |
| Prototype | `set_prototype_links {links:[{nodeId, to}]}`: back arrows, rows and tabs go to their screens in Present mode |
| Screens | Fixed size (390x844, 1440x900...); content longer than the screen scrolls by itself (the main column under a fixed sidebar and header, else the screen), or set `overflow: auto` on the frame that should scroll; it keeps its size and the overflow scrolls on the canvas and in Present (inspector: Scroll overflow; page: Screens scroll) |
| Project | `rename_file {name}` (open project stays open), `move_nodes {toPageId}` keeps instances linked and notes attached |
| Shared libraries | `publish_library`, `list_libraries`, `enable_library`, `update_library`, `disable_library` |
| Icons | `search_icons`, `<x-paper-icon name="set:name">`; CLI: `papercraft-cli icons add\|list\|search` |
| Design in a library's look, hand off its code | `apply_ui_kit` (shadcn, antd, reactbits, egui), then `<x-paper-instance>` of its components; `set_code_link` for a component of your own; `get_jsx` / `get_design_spec` then write `<Button variant="outline">` instead of divs |
| Design in a library's look, hand off its code | `apply_ui_kit` (shadcn, mui, mantine, bootstrap, chakra: ~48 components and 5 screen layouts each, re-colour through their variables; antd, reactbits, egui), then `<x-paper-instance>` of its components; `set_code_link` for a component of your own; `get_jsx` / `get_design_spec` then write `<Button variant="outline">` instead of divs |
| Avatars, more icons, photos, flags, QR | `<x-plugin use="avataaars\|dicebear\|iconify\|picsum\|flags\|qr" …/>` inside `write_html` (plugins the user turned on) |
| References and art | `fetch_reference_images`, `zoom_image`, `generate_image` (when the user turned image generation on) |
| Ask the user | `ask_user` (clickable answers; end your turn after it) |
| Hand work to the helper agent | `delegate` (`role` qa, read or build, a self-contained `task`; only when Settings, Chat, Helper is set) |
| Design rules (design-playbook) | `design_rules` (`query` or `rule` id; needs the `playbook` CLI; cite ids in the report) |
| Report a PaperCraft problem | `report_issue` (a draft the user sends; synthetic repro only) |
| Check quality | `lint_design` |
| Export | `export` (PNG, SVG, PDF to files), `export_combined_pdf` |
| Check quality | `lint_design` (`changedOnly: true` or `since` between edits, a full pass at the end; `overflow` flags layers past their artboard or clipping parent) |
| Export | `export` (PNG, SVG, PDF to files), `export_combined_pdf`, `export_presentation` (a client deck: cover, contents, a slide per screen) |
| Point at things for the user | `select_nodes` (`zoom: true`) |
| Take back | `undo`, `redo` |
| Save | `finish_working_on_nodes` |
@@ -156,7 +168,7 @@ Full flows: [recipes.md](references/recipes.md).
| `papercraft-cli` exits 1, "isn't running" | The app is closed, no project is open, or the port differs. Start it and open a project; check `PAPERCRAFT_MCP_PORT`. |
| Call fails with an unknown node id | The id is from another page or was deleted. Re-read with `get_children`; pass `pageId`. |
| `write_html` reports `ignoredStyles` | You used a CSS property that has no equivalent (`margin`, `box-shadow`, `transform`...). Replace it, see [html-subset.md](references/html-subset.md). |
| Content is cut off | The artboard height is fixed. `update_styles` with `height: "fit-content"` or a larger number. |
| Content is cut off | A frame inside the screen clips it (`overflow: hidden`, or a fixed height under a hugging parent). Give the column that should scroll `overflow: auto`; for a board that should grow, `overflow: "visible"` and `height: "fit-content"`. |
| A color did not change with the theme | It was written as hex, or it is a stroke or gradient. Use `var(--name)` on fills and text; strokes need `update_styles` after a mode switch. |
| `create_instance` fails | Wrong `componentId` / `componentKey` (read `get_components`). Mains on other pages and enabled libraries work; no import needed. |
| A page is full of copied components at y=0 | Something imported mains onto the page (`import_components`, or old `write_html` builds). Swap their instances to the design-system mains (`swap_instance {componentKey}`), check a screenshot, then delete the unused copies. |
@@ -21,7 +21,7 @@ How to keep designs small, keep design and code in step, and spend few tokens do
1. Design is the source of truth for structure, tokens and copy. Code is generated or verified from it, never the other way round without updating the design.
2. `get_design_spec {nodeId}` for each artboard: one deterministic JSON (layout, sizing, tokens, icons, `instanceOf`, `componentDefinitions`). Hand that to the implementer, not screenshots.
3. Implement from the spec. `verify_code {code, nodeId, language}` after each file; fix what it reports. Re-run `get_design_spec` only for artboards that changed (`diff_frames` between the old and new version of an artboard names the changed properties).
3. Implement from the spec. `verify_code {code, nodeId, language}` after each file; fix what it reports. Re-run `get_design_spec` only for what changed: `mark_handoff {name}` when screens go to development, then `get_handoff_changes {since: name}` lists each changed screen, component and token with its kind and action, and `export_handoff {dir, since}` writes the bundle (specs, reference images, tokens, changes.md) rewriting only changed files.
4. Name code pieces after components: `Button / Primary` becomes `button::primary` or `<ButtonPrimary>`. A 1:1 name map is the cheapest way to stay synced; keep it in a table in the feature spec (`specs/<feature>/design-map.md`).
5. Screenshots only for the final visual check (one per artboard, scale 1), and for diffing code output against the design (`get_screenshot` of both, compare).
@@ -37,6 +37,6 @@ How to keep designs small, keep design and code in step, and spend few tokens do
## Sharing and several agents
* Several agents may work on one open project through one MCP server: calls run one at a time, each is one undo step, so they do not corrupt each other. Give each agent its own page or artboard prefix and say so in the brief, so they do not restyle each other's nodes.
* Several agents may work on one open project through one MCP server: calls run one at a time, each is one undo step, so they do not corrupt each other. Give each agent its own page or artboard prefix and say so in the brief, so they do not restyle each other's nodes. Each agent names itself (`papercraft-cli --as <name>`), claims what it rewrites (`claim_nodes`), reads the conflicts its writes report, and undoes only its own steps.
* Several people on a shared project: pages sync as files and are merged when both sides changed the same page (nodes by id, child lists as sets). Edits to different nodes both survive; the same property changed twice keeps the local value. Ask people to keep a page per person when restyling the same frame.
* While an agent works the app shows an animated "working" pill and a ring around the canvas; call `finish_working_on_nodes` when done so it stops.
+4 -2
View File
@@ -28,14 +28,16 @@
Supported: `display:flex` (and `inline-flex`), `flex-direction`, `flex-wrap`, `gap`, `row-gap`, `column-gap`, `padding` (shorthand, per side, `padding-inline`, `padding-block`), `align-items`, `justify-content` (including `space-between`), `align-self` (`start`, `center`, `end`, `stretch`), `flex-grow` / `flex: 1`, `width`, `height` (px, %, `fit-content`), `min-width`, `max-width`, `min-height`, `max-height`, `overflow:hidden` (clips), `position:absolute` with `left`/`top`/`right`/`bottom`, `display:none`, `visibility`.
Not supported (check `ignoredStyles` to be sure): `margin` (use `gap` and `padding`), `display:grid`, tables, `float`, `calc()`, viewport units, `transform` and rotation.
Rewritten before layers are made, each reported in `warnings`: `margin` becomes the parent's `gap` (when uniform) or spacer layers, `margin-*:auto` a growing spacer and `margin:0 auto` centring; `display:grid` becomes rows of cells (`fr` tracks grow, px tracks keep their width, `repeat(auto-fill, minmax())` counts columns from the width); inline boxes (chips, badges) become boxes in a wrapping row; `<input>` and `<textarea>` become a box showing their placeholder or value. Prefer `gap` and `padding` anyway: the result is the same and shorter.
Not supported (check `ignoredStyles` to be sure): tables, `float`, `calc()`, viewport units, `transform` and rotation.
Tips:
* Artboards are `display:flex; flex-direction:column`. Children of a column stretch to the full width unless they have a width or `align-items` / `align-self` says otherwise.
* To center something, put it in a flex parent with `align-items:center; justify-content:center`.
* To make a row fill the remaining space, give the child `flex-grow:1`.
* Use `height:"fit-content"` on artboards that should grow with content; fixed heights clip.
* A screen keeps its device's size; at a device width (390, 1440...) `height:"fit-content"` becomes the device height. Content taller than the screen scrolls by itself: the main column under a fixed sidebar and header, else the screen (write_html says which). Pass `overflow:"visible"` to let a board grow (a landing page drawn in full).
## Text
@@ -0,0 +1,50 @@
# Incremental checks
Check what you just changed between edits, and check everything once at the end. This keeps the loop fast and the context small without skipping the final full pass.
## Why
* A full `lint_design` on a big page walks every layer and returns every finding. Run after each edit, it repeats the same output and burns tokens.
* Looping `get_node_info` over nodes is slower still: one measured loop over 397 nodes took about 53 s. One `get_bounds` call returns the same geometry for a whole subtree in milliseconds, in one result.
* Reads never change the document, so they never change `rev`. Only writes do.
## The flow
1. **Note `rev`.** Every write tool result includes `rev`, the document revision after the call. `get_basic_info` reports the current one. Keep the last value.
2. **Edit.** Make the change (`write_html`, `update_styles`, `set_text_content`, ...). Note the new `rev` from the result.
3. **Lint what changed.** `lint_design {"changedOnly": true}` checks since this client's last lint. `lint_design {"since": <rev>}` checks since a revision you name. A scoped lint covers the touched layers plus their ancestors, descendants and siblings, and every layer bound to a changed variable. The result says how much it covered, for example `{"checked": 12, "total": 397, "since": 40, "partial": true}`.
4. **See what an edit really touched.** `get_changes {"since": <rev>}` returns `{rev, since, truncated, changes: [{page, ids}]}`. Add `page` to limit it to one page. If `truncated` is `true`, the log no longer reaches that far back: do a full pass instead of trusting the list.
5. **Measure in one call.** `get_bounds {"nodeIds": [...]}` (or `pageId`, with `depth`) returns `columns` `[id, parent, x, y, w, h, absX, absY, clip]` and `rows`, one array per layer, parents first. If `truncated` is `true`, call it again with `continueWith` as `nodeIds` (same `depth`). Never loop `get_node_info` to read sizes or positions.
6. **Finish with the full check.** Before you report, always:
* run `lint_design` with no arguments (a full pass, `partial: false`) and fix every error and warning, including `overflow`;
* take a `get_screenshot` of every artboard you touched in each theme mode (`mode`, scale 2).
A partial lint is a speed-up, not a verdict. Only the final full pass can say the design is clean.
## Example
```json
get_basic_info {}
// {"rev": 40, ...}
write_html {"targetNodeId": "a1:1", "mode": "insert-children", "html": "<div class='Card'>...</div>"}
// {"rev": 41, ...}
lint_design {"changedOnly": true}
// {"errors": 0, "warnings": 1, "covered": {"checked": 12, "total": 397, "since": 40, "partial": true}, ...}
update_styles {"nodeIds": ["a1:7"], "styles": {"width": "320px"}}
// {"rev": 42, ...}
get_changes {"since": 41}
// {"rev": 42, "since": 41, "truncated": false, "changes": [{"page": "p1", "ids": ["a1:7"]}]}
get_bounds {"nodeIds": ["a1:1"], "depth": 3}
// {"columns": ["id", "parent", "x", "y", "w", "h", "absX", "absY", "clip"],
// "rows": [["a1:1", null, 0, 0, 390, 844, 0, 0, true], ...]}
lint_design {}
// full pass, "partial": false, zero errors and warnings
get_screenshot {"nodeId": "a1:1", "scale": 2, "mode": "light"}
get_screenshot {"nodeId": "a1:1", "scale": 2, "mode": "dark"}
```
+1 -1
View File
@@ -57,7 +57,7 @@ For the next screens, create each artboard at 390x844, reuse the components by `
## 2. Landing page (1440 wide, height grows with content)
Create the artboard with `height:"fit-content"` so it never clips, then add one section per call.
Create the artboard with `height:"fit-content", overflow:"visible"` so it grows with its sections instead of keeping the 900px screen height, then add one section per call.
```sh
AB=$(papercraft-cli call create_artboard '{"name":"Landing","styles":{"width":"1440px","height":"fit-content","backgroundColor":"var(--bg)","display":"flex","flexDirection":"column","paddingBottom":"96px"}}' | jq -r .id)
+125 -5
View File
@@ -79,6 +79,17 @@ Details of one node: type, name, size, visibility, lock state, parent, children
| `nodeId` | string | yes | The node to inspect. |
### `get_bounds` (read-only)
Rects of a whole subtree in one read, instead of one get_node_info per layer. Returns `columns` [id, parent, x, y, w, h, absX, absY, clip] and `rows` (one array per layer, parents before children): x/y/w/h are relative to the parent, absX/absY are page coordinates, clip is true when the layer clips its content (clipping or scrolling frame), parent is null for a top-level layer. At most 2000 rows per call: then `truncated` is true and `continueWith` lists the node ids to pass as nodeIds. Use it to verify nothing leaves its artboard, and `lint_design`'s `overflow` rule to list the offenders.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `depth` | number | | Levels returned, counting each given node as 1 (default: all). |
| `nodeIds` | string[] | | Subtrees to measure; omit for all artboards on the page. |
| `pageId` | string | | Page to use (default: the active page). |
### `get_children` (read-only)
Direct children of a node (or of the page root when nodeId is "root"), with ids, names, types, child counts and positions.
@@ -207,6 +218,7 @@ Render a node (or the whole page when nodeId is omitted or "root") to a PNG and
| Argument | Type | Required | Notes |
|---|---|---|---|
| `annotations` | boolean | | Also draw the note cards, badges and leader lines of notes on the captured layers; the image grows to include the cards. |
| `mode` | string | | Render in this variable mode (e.g. "Light") without switching the document: preview every theme of one artboard while other agents keep working in the current mode. |
| `nodeId` | string | | The node to capture; omit for the whole page. |
| `out` | string | | Optional file path to save the PNG to (also used by papercraft-cli). |
| `scale` | number | | Render scale: 1 (default) or 2. |
@@ -232,11 +244,61 @@ Export several nodes into one PDF, one page per node, ordered top-to-bottom then
| `path` | string | | Output file (default: Downloads/<project>.pdf). |
### `export_presentation`
Export pages as a presentation PDF, the way a designer sends work to a client: a cover, contents, a divider per page, one 1920×1080 slide per screen (its name and size; narrow side panels share a slide) and a closing slide, numbered. Screens are the page's declared screens, else its top-level frames of 300×300 or more; a section holding no screen (a design-system board) gets a slide of its own. They stay vector. Returns the file path.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `pageIds` | array of string | | Pages to include, in this order (default: every page). |
| `path` | string | | Output file (default: Downloads/<title>.pdf). |
| `title` | string | | Deck title (default: the project name). |
| `subtitle` | string | | Cover subtitle (default: screen and page counts). |
| `author` | string | | Shown on the cover after the date. |
| `language` | string | | Language of the deck's own words and date: en, pt, es, fr, de, it (default: the one the screens are written in). |
### `mark_handoff`
Mark a handoff point: a named snapshot of every page, taken when screens go to development. get_handoff_changes and export_handoff `since` then report what changed against what the developers received. Marking a name again moves it to now. Saved in the project (handoffs/). Returns {name, pages, screens}.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `name` | string | yes | Name of the point, e.g. "design v1" or a release tag. |
### `list_handoffs` (read-only)
The project's handoff points (mark_handoff), newest first: [{name, saved}].
### `get_handoff_changes` (read-only)
What changed since a handoff point, sorted the way code acts on it, in few tokens: token values once (regenerate the theme), main components once with the screens using them (instances follow), then screens: added, removed, renamed, and per changed layer its kind (copy, binding, style, layout, structure, instance, note) and `field: old → new` with token names. Sizes that follow content, positions set by auto layout and instance fields that follow their main are left out. Markdown shows 8 changes per screen; pass `nodeId` (a screen or component key from it) for all of one. Layer keys work with get_design_spec {nodeId, format: compact} to fetch just that subtree.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `format` | `markdown` \| `json` | | markdown (default, an index) or json (everything). |
| `nodeId` | string | | Only this screen or main component, every change. |
| `since` | string | yes | Handoff point name (list_handoffs). |
### `export_handoff`
Write the handoff bundle a code base builds from into a folder: index.md (one line per screen with its spec file and size in tokens), inventory.json, tokens.json (DTCG with modes), tokens.css, tokens.tailwind.css, components.json, open-questions.md (`? (team)` notes and the table on a page named Handoff), and per screen a compact spec (screens/<page>/<screen>.txt) and a reference PNG at scale 1 (a scrolling screen shown whole). With `since`, also changes.md and changes.json (get_handoff_changes). manifest.json lists each file's hash and tokens. Only files whose bytes changed are written, and files a previous export wrote that no longer exist are removed; nothing else in the folder is touched. Returns {dir, files, written, removed, screens, images}.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `dir` | string | yes | Folder to write into (created if missing), e.g. docs/design. |
| `images` | boolean | | Write reference PNGs (default true). |
| `since` | string | | Handoff point to report changes against. |
## Write
### `create_artboard`
Creates an artboard (top-level frame) and returns its id; then add content with write_html({mode: "insert-children"}). `styles` are camelCase CSS; width and height are required (e.g. "1440px"). Artboards default to display:flex, flexDirection:column and a white background, and are placed in an empty spot unless left/top are given. Default sizes: desktop 1440×900, tablet 768×1024, mobile 390×844.
Creates an artboard (top-level frame) and returns its id; then add content with write_html({mode: "insert-children"}). `styles` are camelCase CSS; width and height are required (e.g. "1440px"). Artboards default to display:flex, flexDirection:column and a white background, and are placed in an empty spot unless left/top are given. Default sizes: desktop 1440×900, tablet 768×1024, mobile 390×844. At a device width, height "fit-content" becomes the device height; content taller than a screen scrolls in its main column (else the screen). overflow "visible" lets the board grow instead.
| Argument | Type | Required | Notes |
|---|---|---|---|
@@ -248,7 +310,7 @@ Creates an artboard (top-level frame) and returns its id; then add content with
Parse HTML with inline styles into layers. Write incrementally: each call should add one visual group (a header, one list row, a card) so the user sees progress.
- mode "insert-children" appends the HTML inside targetNodeId ("root" = the page); "replace" replaces targetNodeId with it.
- Inline styles only. Flexbox (display:flex, flex-direction, gap, padding, align-items, justify-content, align-self, flex-grow, flex-wrap) is the layout model; block elements stack vertically. position:absolute with left/top is supported. No margin, grid or tables.
- Inline styles only. Flexbox (display:flex, flex-direction, gap, padding, align-items, justify-content, align-self, flex-grow, flex-wrap) is the layout model; block elements stack vertically. position:absolute with left/top is supported. Margins become gaps or spacers, display:grid becomes rows of cells, text inputs show their placeholder (each reported in warnings). No tables.
- Text: font-size, font-weight (≥600 = bold), font-style, color, line-height, letter-spacing, text-align, text-decoration (including wavy and text-decoration-color); <b>/<i>/<u>/<s>/<span style> make rich text. Font family is always Inter.
- Boxes: background(-color/-image: colors, linear/radial gradients, url()), border (and border-top/right/bottom/left), border-radius, opacity, overflow:hidden, mix-blend-mode, width/height (px, %, fit-content), min-width/max-width/min-height/max-height.
- <img src> takes paper-asset:///abs/path, file:// URLs, absolute paths or data: URIs. <svg> markup is imported as vector layers.
@@ -502,9 +564,31 @@ Search installed open icon sets (Lucide, Tabler, Phosphor, Heroicons, Material).
| `set` | string | | |
### `critique_design`
Critique finished work the way a design team does (offer it with ask_user when finish_working_on_nodes says the design changed). Judges against the person's objective, through independent reviewers: vision (the person's brief and notes), ux (Nielsen's heuristics), flow (coherent journeys across screens), qa (states and edge cases), visual, accessibility (W3C POUR), copy and end-user (a walkthrough as the audience). Returns the reviewers' briefs, the finding format and 0-4 severity scale, lint, the arrows between screens (dead ends, unreached screens), the person's lessons and the steps: review each persona independently, merge, fix severity 3-4, report, then ask which findings to keep as lessons.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `nodeId` | string | | Artboard or subtree to critique; omit for the whole page. |
| `objective` | string | | What the design must achieve, for whom, and its top tasks (from the brief or the person). |
| `audience` | string | | Who the end-user reviewer plays ("first-time shopper on a phone"). |
| `personas` | array of string | | Reviewers to run (default: all): vision, ux, flow, qa, visual, accessibility, copy, end-user. |
### `design_lessons`
The person's design lessons: rules from past critiques that every design must follow (get_basic_info lists them too; read them before designing). Without arguments, lists those that apply in the open project. `add` stores one ONLY after the person approved it in this conversation (ask_user); never on your own. `remove` deletes one by id.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `add` | object | | `{rule, why, scope}`: rule is one line, an instruction; why is what went wrong; scope `all` (default) or `project`. |
| `remove` | string | | Id of a lesson to delete ("L3"). |
### `lint_design`
Check a page or subtree for common design mistakes and get a fix for each: text contrast (WCAG 4.5:1, 3:1 for large text), text under 12px, font sizes off the type scale, gap/padding off the 4px grid, buttons and tabs under 44px tall, default layer names (Frame, Rectangle), and hard-coded colors that equal a variable. Run it before finish_working_on_nodes and fix errors and warnings; `info` items are suggestions. Pass `fix: true` to automatically snap spacing/padding to 4px, relink matching color variables, snap text sizes, and enlarge small touch targets.
Check a page or subtree for common design mistakes and get a fix for each: text contrast (WCAG 4.5:1, 3:1 for large text), text under 12px, font sizes off the type scale, gap/padding off the 4px grid, buttons and tabs under 44px tall, default layer names (Frame, Rectangle), layers or text overflowing their artboard or clipping parent (rule `overflow`, never auto-fixed; scrolling frames are exempt), text too narrow for its words (text-collapsed), and hard-coded colors that equal a variable. Run it before finish_working_on_nodes and fix errors and warnings; `info` items are suggestions. Pass `fix: true` to automatically snap spacing/padding to 4px, relink matching color variables, snap text sizes, and enlarge small touch targets.
| Argument | Type | Required | Notes |
|---|---|---|---|
@@ -673,12 +757,48 @@ Move the user's view: switch to pageId (as clicking it in the page list does) an
### `undo`
Undo the last change on a page (the user's or an agent's).
Undo a change. An agent's undo (scope `mine`, its default) takes back its own newest step and leaves later steps by others in place; it is refused (`undo_blocked`, with the steps in the way) when one of them touched the same layers. Scope `page` undoes the page's last step, whoever made it.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `scope` | string | | `mine` or `page`. |
### `redo`
Redo the last undone change on a page.
Redo the last undone change on a page. With scope `mine` (an agent's default) only when that step was the caller's own.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `scope` | string | | `mine` or `page`. |
### `claim_nodes`
Several agents on one project: claim the layers you will rewrite (each with everything under it) or a shared resource, so other agents' writes there are refused while you work. All or nothing; a denial names the holder and when it expires (never wait in a loop: do other work). The claim lasts ttlSeconds after your last call and goes with finish_working_on_nodes or release_claims. The person is never blocked. Needs the running app.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `ids` | array of string | | Layer ids. |
| `resources` | array of string | | `variables`, `components`, `pages`. |
| `ttlSeconds` | integer | | Default 300 (10 to 1800); renewed by each of your calls. |
| `note` | string | | What you are doing, shown to others. |
### `release_claims`
Release your claims: one by claimId, those on ids or resources, or all of them when nothing is given.
| Argument | Type | Required | Notes |
|---|---|---|---|
| `claimId` | string | | |
| `ids` | array of string | | |
| `resources` | array of string | | |
### `list_claims` (read-only)
Who is working where: every live claim with its holder, layers, resources, note and seconds left.
### `finish_working_on_nodes`
+46
View File
@@ -0,0 +1,46 @@
# Component properties
## Why
A status badge, a tab bar or a list row is drawn once as a component, but every use differs:
the status is Autorizada, Rejeitada or Na SEFAZ; a different tab is active; the label and the
icon change. Today each case is its own component (`Badge / Green`, `Tabs / Lojas`, `Tabs /
Pedidos`) or hand edits on layers, so a design needs a component per state and an agent edits
layer by layer.
## What
1. **Properties live on the main component** (`Node.props`), each with a name and a type:
* `text`: the text of a layer (`layer`).
* `bool`: shows or hides a layer.
* `icon`: the icon of an icon layer (`Icon / set:name`), any `set:name`.
* `enum`: a list of options; each option is a set of changes in the `overrides` format
(`{"Label": "Rejeitada", "$root": {"background": "var(--danger-bg)"}, "Dot": {"icon": "lucide:x"}}`),
so an option can restyle, retext, show, hide or re-icon any layer. Fully custom.
2. **Instances keep their values** (`Node.prop_values` on the instance root) and apply them as
overrides, so edits to the main keep them; editing the definitions re-applies every
instance's values on every page.
3. **Templates** make the common cases one call: `text` (a text property per text layer),
`active` (an enum choosing which item of a row, tab bar or nav is active, with active and
inactive styles) and `status` (an enum of statuses, each a colour that tints the root and
colours its text and icon).
4. **Where to use it**: `set_component_props` (define, or from a template),
`set_instance_props` (many instances in one call), `get_components` (lists properties),
`<x-paper-instance component="Badge" props='{"Status":"Rejeitada"}'/>` in `write_html`,
the inspector (a control per property on an instance; templates on a main), and code
links (`{Status}` in a usage template is the instance's value).
## Not now
Variant sets made of several mains, nested property exposure (a row exposing its badge's
properties), numeric properties.
## Acceptance
* A badge with an enum Status (Autorizada, Rejeitada) shows the option's colours and label on
each instance; switching the value updates only that instance.
* Editing the main (gap) keeps every instance's values; editing an option's colour updates
every instance using it.
* The `active` template on a five-item tab bar makes one property whose options are the item
names; each instance shows its active item highlighted.
* An icon property swaps the icon and survives a sync of the main.
+42
View File
@@ -0,0 +1,42 @@
# Design critique and lessons
## Problem
When an agent finishes a design it says it is done; nobody reviews the work unless the person asks,
and the `papercraft-critique` skill only reaches agents that load skills (not the in-app chat). The
same mistakes come back in the next design (a page that grows instead of scrolling, gendered copy,
clipped labels) because nothing the person corrected is remembered.
## Goal
After a design is finished the person is offered a critique; the corrections they approve are
remembered and shown to every agent before its next design, so the same mistake is not repeated.
## Non-Goals
- Scoring by a model inside the app: the agent does the judging, the tool gathers the evidence.
- Saving anything the person did not approve.
- A settings screen for lessons (the tool lists and removes them).
- Syncing lessons between machines.
## User Stories
- As a designer, I want to be offered a critique when an agent finishes, so that I don't have to remember to ask.
- As a designer, I want to choose which critique findings become lessons, so that only what I agree with sticks.
- As a designer, I want agents to follow my lessons in later designs, so that I don't correct the same thing twice.
- As a designer, I want lessons for one product to stay in that product, so that a brand rule does not leak elsewhere.
- As a designer, I want to remove a lesson, so that a rule I no longer want stops applying.
## Acceptance Criteria
- [ ] Given edits since the last critique, when an agent calls `finish_working_on_nodes`, then the result tells it to offer the person a critique (`ask_user`), and it does not after a critique with no edits since.
- [ ] Given a page or artboard, when `critique_design` runs, then it returns the objective (or that it is missing and must be asked), the chosen reviewers (vision, ux, flow, qa, visual, accessibility, copy, end-user) with their methods, the finding format and 0-4 severity scale, the lint, the flow between screens and the lessons that apply.
- [ ] Given screens joined by arrows, when `critique_design` runs, then the flow lists the links, the dead ends (reached, leading nowhere) and the screens nothing leads to or from.
- [ ] Given several reviewers, the steps have them review independently (one subagent each, or one pass each) and a coordinator merge, never debate.
- [ ] Given the person approved a finding, when `design_lessons` adds it, then it is stored in the project store and listed by `get_basic_info` and `critique_design` from then on, in every session.
- [ ] Given a lesson saved with scope `project`, when another project is open, then it is not listed.
- [ ] Given a lesson id, when `design_lessons` removes it, then it is no longer listed.
- [ ] The tool descriptions say a lesson is saved only after the person approved it.
## Constraints
- Stack: Rust, MCP tools in `src/mcp`; lessons in a JSON file at the project store root, written atomically.
- Compat: missing or unreadable lessons file means no lessons, never an error for other tools.
- Security: lessons are plain text the person approved; no secrets.
## Open Questions
- None.
+76
View File
@@ -0,0 +1,76 @@
# Design handoff and design updates
## Problem
Handing a PaperCraft design to code is a wrapper script today (`shipcraft/bin/pc-freeze.py`): one CLI call per screen (186 for Rota Fresca), the Handoff table parsed out of tree text, every file rewritten on each run, no picture of the design, and `verify_code` only against the running app. Worse, once code exists, a design edit has no handoff at all: nobody can tell which screens changed since the code was built, what kind of change it was (copy, a token, a component, the layout, a business rule), or what the code side has to do about it. Rota Fresca's `design-changes.md` shows the cost: nine design changes tracked by hand, each followed by "re-exported" of whole pages, and a stroke-colour question that took a manual check to settle. The same gap applies to any team that is not using shipcraft: there is no "what changed since you last built this" in PaperCraft.
## Goal
From any point the code was built from, PaperCraft says which screens and components changed, how, and what to do about each, in files a person or an agent can act on without opening the app.
## Non-Goals
- Generating or editing application code; PaperCraft describes, the code side implements.
- Git, issue-tracker or chat integrations; outputs are files and JSON others can feed into them.
- Merging two design branches; shared-project merge already exists.
- Replacing the presentation deck; it stays the owner-facing view.
- Pixel-diffing the running app against the design (shipcraft's `measure:route` keeps that job).
## User Stories
- As a developer, I want one command that writes the whole handoff bundle for a project (inventory, tokens, components, open questions, specs, reference images), so that I build from frozen files and never from the live app.
- As a designer, I want to mark a set of screens as handed off ("Ready for dev") at a named point, so that later edits are measured against what developers actually received.
- As a developer, I want a list of what changed since a handoff point, sorted by screen and by kind of change, each with the action it calls for, so that I update only what moved.
- As an agent building one screen, I want only the changed part of a screen's spec (the subtree and its path) plus before/after images, so that an update costs a few thousand tokens and not a whole-page rebuild.
- As a developer, I want to record which screens are implemented and from which handoff point, so that each screen's next diff starts from what was built for it, not from one global date.
- As a project owner, I want a "what changed" deck (before and after per changed screen, with the reason), so that I can approve an update without reading specs.
- As a team not using shipcraft, I want the bundle to be framework-neutral (DTCG tokens, CSS variables, Tailwind theme, SVG icons, named image assets, per-screen HTML reference), so that any stack can consume it.
## Acceptance Criteria
### Bundle
- [ ] Given a project, when the handoff export runs into a folder, then the folder holds the inventory, tokens (DTCG, CSS, Tailwind), components, open questions (including the Handoff table as structured rows), one compact spec per screen, one reference image per screen per mode, icons as SVG and images as named files, and a manifest listing every file with a content hash.
- [ ] Given a screen whose content scrolls, when it is exported, then its spec marks the scrolling frame and its full content height, and its reference image shows the whole content (as the deck does).
- [ ] Given an unchanged project, when the export runs twice, then the second run changes no file (byte-identical, same manifest).
- [ ] Given a renamed or deleted screen, when the export runs again, then the old files are removed and no stale file remains.
- [ ] Given the app is closed, when the export runs through the offline CLI on the same project, then the output is the same as with the app open.
### Handoff points
- [ ] Given selected screens or pages, when the designer marks them handed off under a name, then PaperCraft stores a baseline of those screens and the tokens and components they use, and the canvas shows each screen's status (draft, ready, changed since handoff, implemented).
- [ ] Given a screen marked ready, when anyone edits it, then its status becomes "changed since handoff" without any extra step.
- [ ] Given the code side reports a screen implemented from a named handoff point (and optionally a commit), when the next change report runs, then that screen is compared against that point.
### Change report
- [ ] Given a handoff point and later edits, when the change report runs, then it lists added, removed and renamed screens, and for each changed screen the changed nodes matched by id (not by position), each with the old and new value.
- [ ] Given a change, then it is classified as one of: copy, token value, token binding, style, layout, structure (nodes added, removed or moved), component main, instance swap, asset, annotation (behaviour or business rule), or screen added, removed or renamed.
- [ ] Given each class, then the report states the action it calls for. For example: copy → update the string key; token value → regenerate the theme, no screen work; component main → rebuild that component, and list the screens that use it with no screen work; layout or structure → rebuild that subtree; annotation → review the rule in the domain model; screen removed → remove the route.
- [ ] Given a token value or a component main changed, then the report lists every affected screen once, under that token or component, and does not repeat the change on each screen.
- [ ] Given a changed subtree, then the report includes that subtree's compact spec and its path from the screen root, and before and after images of the screen with the changed area outlined.
- [ ] Given a change report, then it is written as Markdown for people and JSON for scripts and agents, from the same data.
- [ ] Given a move or resize under 1 px, or a re-save with no content change, then the report lists nothing.
### Token cost
- [ ] Given a change report, then its first section is an index of one line per affected screen or component (name, change classes, action, file to read), and a reader reads detail files only for the items it acts on.
- [ ] Given a token or component change, then it appears once, as a single line naming the token or component, and is not repeated on each screen that uses it.
- [ ] Given a changed subtree, then its spec fragment holds only the changed nodes and the path from the screen root (names only, no styles). Unchanged siblings appear as one line each, or are left out.
- [ ] Given a change, then each value is written as `field: old → new`, using token names rather than resolved values. Values that did not change are not printed.
- [ ] Given a change that needs no screen work (token value, asset swap, annotation), then the report says so and points at no screen spec.
- [ ] Given the bundle, then every compact spec states its size in tokens in the manifest, so a brief can budget before it reads.
- [ ] Given before and after images, then they are cropped to the changed area plus a margin and kept at scale 1, so they are only fetched when needed.
- [ ] Given a one-node copy change on one screen, then the whole report for it (index line plus detail) is under 300 tokens.
### Verification and review
- [ ] Given a bundle on disk and source files, when verification runs against the bundle, then it gives the same verdict as `verify_code` against the live app, without the app running.
- [ ] Given a change report, when the "what changed" deck is exported, then it holds one slide per changed screen with before and after, the change classes, and the designer's note for the change, in the project's language.
## Constraints
- Stack: Rust (PaperCraft core, MCP tools, CLI); outputs are plain files (JSON, Markdown, PNG, SVG, CSS).
- Performance: a full export of 200 screens finishes in under a minute on the user's machine. A change report for one edited screen costs the reader under 2k tokens; the index for a 50-change update is under 3k.
- Token economy is a requirement, not polish: every output is read by agents billed per token. Fewer, smaller files read on demand beat one complete file.
- Data safety: baselines are stored in the project and migrate with it; exports write atomically; nothing is written outside the chosen folder.
- Compat: works offline (CLI) and online (app); the bundle format is versioned so a reader can refuse an unknown version.
- Tool count: each new MCP tool needs a test in `tests/mcp.rs`, and the README and ROADMAP counts updated.
## Open Questions
- Should a handoff point cover the whole project or only the screens selected? Per-screen baselines are more precise but heavier to store.
- How should "implemented" be reported back: an MCP tool the code side calls, a file in the bundle the export reads (for example `implemented.json` committed in the app repo), or both?
- Which counts as the action for a copy change in a team without a string table: edit the text in place, or always the string key?
- Should the canvas status badges ("changed since handoff") show to every viewer, or only in a "dev" view?
- Is a static handoff viewer (one HTML file in the bundle that browses screens, specs, values and changes) wanted, or are the Markdown and images enough?
- What retention applies to old baselines (keep all, keep the last N, keep the named ones)?

Some files were not shown because too many files have changed in this diff Show More