Commit graph

19 commits

Author SHA1 Message Date
Danila Poyarkov 9dce9fdcbc
feat(collab)!: sync layer trees as a CRDT and open each room in its own tab (#902)
* fix: stop ancestor walks from hanging on a parent cycle

A collaborator's concurrent reparent can leave two layers as each other's
parent. isDescendant, the design check's pageOf, and component sync walked
parentId without a bound, so applying such a change froze the editor.

Add SceneGraph.closest(), a bounded nearest-ancestor lookup, and use it for
these walks so bad data ends the walk instead of the tab.

* fix(collab): sync layer moves without parent cycles or stale child lists

Remote changes assigned each layer's synced parentId and childIds as plain
properties. Two peers moving layers into each other made them each other's
parent, a moved layer stayed listed under its old parent, reorders never
synced, and concurrent additions ended up in different orders or missing
from the parent's list.

Apply the tree after a change's properties: move layers to their synced
parents, skip a move that would make a layer its own ancestor and write the
layer's current parent and position back so every peer settles on it, and
derive each touched parent's childIds from its synced order followed by
unlisted children by id. Locally, a parent's child list syncs once after
each edit that adds, removes, moves, or reorders its children.

Fixes #888
Fixes #889

* refactor(scene-graph)!: move sibling order keys to scene-graph

Collaboration needs the same fractional keys as .fig export to order
siblings, and the app must not depend on @open-pencil/fig for them. Move
fractionalPosition, orderKeyBetween, and siblingOrderKeys to
@open-pencil/scene-graph/order-keys.

orderKeyBetween now always returns a key: when no printable key fits it
returns one above lo, which hasOrderKeyBetween detects, so callers no
longer branch on null. It takes an optional suffix, and siblingOrderKeys
can request one per key, so two peers inserting at one spot get distinct
keys. .fig export keeps its keys.

* fix(scene-graph): report instance child reorders as graph events

Instance sync sorted an instance's children in place, so nothing that
listens to graph events saw the new order; in a shared room the order
never reached other peers. Move each child that changes position with
insertChildAt, which reports the reorder.

* feat(collab): resolve the layer tree from each layer's parent history

Add a pure LayerTree for the shared document: each layer records every
parent it was moved under with a move counter and an order key. A layer
sits under its newest parent, ties broken by parent id; when concurrent
moves close a loop, the latest move in the loop falls back to the
layer's next entry until none is left, and a layer no parent can take
goes to its page (Evan Wallace's mutable tree hierarchy CRDT).

The result depends only on the entries, and resolution revisits only
changed, orphaned, and displaced layers. Seeded random runs check that
peers converge and that the incremental result matches a full one.

* fix(collab): sync layer moves as parent history and order keys

Each layer's shared map now records every parent it was moved under with
a move counter from a document-wide Lamport clock, and its order key
among siblings, instead of parentId and childIds. Every peer derives
parentId and childIds from these with LayerTree, so concurrent moves,
reorders, and additions merge, a loop from concurrent moves undoes its
latest move, and a layer whose new parent was deleted meanwhile returns
to its previous one.

A local edit's graph events are written once after the edit, in one
transaction. It records a parent entry for each layer whose parent
changed, a new entry for displaced layers on the touched paths so a move cannot pull
them back, and keys between the moved layers' neighbours with a random
suffix, so concurrent inserts at one spot get distinct keys. Remote
changes find their layers through each event's path, resolve only what
they touch, and move and sort layers through insertChildAt.

This replaces the childIds merge and the write-back of rejected moves:
a rejected move now resolves the same way on every peer from the shared
history, so nothing needs to be written back.

Fixes #888
Fixes #889

* feat(collab)!: convert saved rooms and keep mismatched builds apart

A room saved by an earlier build records parentId and childIds. When a
change brings in such layers, from this browser's storage or a peer,
convert them in one transaction: each layer's synced parent becomes its
only entry with counter 0, its position in the parent's synced childIds
becomes an order key, and the old fields go. The result depends only on
the document, so two peers converting at once write the same values,
and converting again writes nothing. The document's meta map records
treeFormat 2.

Builds that record the tree differently would corrupt each other's
rooms, so the collaboration namespace becomes openpencil/2 for Trystero
and the test relay alike, and each peer publishes its treeFormat in
awareness for future version messages.

* fix(collab): send a layer whose parents were all deleted back to its own page

Each layer's shared map records the page it was last placed on, and the
layers under a frame moved to another page are re-recorded. A layer whose
parent chain was deleted goes back to that page, falling back to the first
page only when the recorded one is gone. Saved rooms record pages when
they are converted.

* fix(collab): keep one root per room when peers edit their own documents

Joining a room keeps the joiner's earlier document in its graph, and an
undo or an edit made before the room arrived could still reach it. That
edit shared the joiner's root, every peer adopted it, and the room's
pages disappeared.

The room now records its root as claims in meta, each with the time it
was made, and every peer follows the earliest. Sharing claims the room,
and so does the first edit in a room nobody has shared, which now shares
the whole document as Share does. A peer shares only layers under the
room's root. Converted rooms claim the root with the most children.

Move counters must also be safe integers, so an oversized counter from
another peer cannot stop the move clock from advancing.

* fix(collab): rank root claims by how they were made, not by clocks

Root claims carried the claiming peer's wall-clock time, so a guest whose
clock ran behind the sharer's could still win the room with an edit made
before the room reached them. A claim now records whether it came from
Share or a converted room, or from the first edit in an unshared room;
a shared root outranks an edited one, and the lower id breaks a tie.
A claim also replaces an invalid value already stored for its root.

* fix(collab): keep every root claim through concurrent writes

A root claim was one key per root holding its kind, so two peers claiming
the same root by Share and by an edit at once kept only one of the two
values, and the shared claim could be lost. Each kind of claim on a root
is now its own key. Converting a saved room also claims its root unless
a shared claim exists, so a guest's earlier edited claim no longer keeps
the converted room from outranking it.

* fix(collab): mint layer IDs under a session of each editor window

Every editor window started its IDs at 0:1 from the same counter, so two
people adding layers to a shared room at once could mint the same IDs,
and one person's layers replaced the other's in the room. A joiner's
starting page also took the sharer's page ID and stayed in their list.

SceneGraph's default IDs now carry a session set with setIdSession, as
in Figma's sessionID:localID GUIDs. The editor picks a random 32-bit
session at startup, as Yjs does for each document's clientID; headless
tools keep session 0, so the CLI and MCP server give a file's layers the
same IDs on every run.

* fix(collab): let only Share set a room's root

A guest's first edit in a room whose contents had not arrived claimed
the room and wrote the guest's whole open document into it, images
included, and adopting the sharer's root later only hid it. Every room
starts with someone sharing a document, so a guest has nothing to claim:
only Share, or converting a saved room, now sets the room's root, as a
single value in meta, and a peer writes nothing until the root is known.

Unbinding a room also writes an edit still waiting to be sent, so a move
or deletion made just before leaving reaches the room.

* feat(collab)!: open each room in a tab of its own

Joining a room bound it to whatever tab was active, so a pasted link
could turn a saved file into the room's document, and a share link first
showed an editable blank document. A room is now a document: joining
always opens it in a new tab, or switches to the tab already showing it,
and only Share puts an existing tab's document into a room.

Every room tab owns its session (src/app/collab/rooms.ts and
session.ts), so several rooms can be live at once and keep syncing in
the background. The collaboration panel, presence, following, and the
/share/<id> address follow the active tab, and a canvas publishes its
cursor and selection only to its own tab's room.

A room tab derives its state: joining while its saved copy loads, then
waiting, with an explanation, while nobody who has the file is online;
live with others, or alone on this device's copy. Until the document
arrives the room's screen replaces the editor. Reloading a share link
rejoins it; leaving a room you joined keeps its file as a local unsaved
copy. "Connected" now means another peer answered. Pasted links and IDs
are normalised and validated, and invalid ones say so.

People join right away under a generated name such as "Teal Fox", with
a hint to set one; the one app-wide name is set in the share panel or
in Settings. On a phone, Share copies the room's link instead of making
a new room, and the presence popover shows the room's state.

* feat(desktop): open rooms from openpencil://join links and Home

The desktop app could not receive a share link: links point at the web
app, and openpencil:// only opened files. openpencil://join?room=<id>
now opens the room in a tab of its own. The native parser refuses
anything but a room ID, queues rooms for the frontend through
take_pending_rooms, and a second launch on Windows and Linux forwards
its link through the single-instance handler.

In a browser on a computer, the room's screen and the share panel offer
Open in desktop app, a link the browser hands to the app on click; it
never opens the app by itself. Home gains Join room…, which takes a
pasted room link or ID and opens the room in a new tab.

* feat(collab): set your name on the room screen

The room screen told someone joining under a generated name to set
their name but offered nowhere to do it before the file arrived. It now
has the same name field as the room panel.

* feat(collab): offer the desktop download beside Open in desktop app

A browser on a computer offers a room's openpencil://join link, which
does nothing where the app is not installed. The room screen and panel
now link to the latest release beside it.

* docs: describe joining rooms in the German, Polish, and Russian guides

Bring the translated collaboration pages up to the English one: Share as
the only way into a room, joining in a tab of its own, the waiting
screen, leaving with a local copy, and how layer moves merge. The
English page now names the panel's Leave room button.

* fix(collab): lay out the room screens like the app's empty states

The joining and waiting screens were a left-aligned card with a stray
spinner, a primary Copy link button beside an outline button and a
bare link, and a name field on a screen that lasts seconds. They now use
AppPlaceholder, centred over the tab: a heading, the explanation, the
two hints, secondary Copy link and Leave, a 'You'll appear as' line
whose Change opens a small rename popover, and the desktop handoff on
one muted line. The room panel lines its status dot up with wrapped
text, no longer selects the room link when it opens, and puts the
desktop links and Leave room on one footer line.

* fix(collab): show what a room tab is doing instead of a timed guess

A joined tab said nobody with the file was online five seconds after it
opened, whether or not it had reached the signaling service or met the
people already in the room. Its state now follows what the tab can
observe: connecting until the service answers, looking for people for as
long as that transport takes to introduce everyone, getting the file
from someone who says they have it, waiting when nobody who has it
showed up (naming other guests waiting too), and a can't-connect screen
when the service cannot be reached. Each peer says in its presence
whether it has the room's file.

* fix(collab): list other waiting guests with the explanation

The line naming other guests who are waiting too is information, not an
action, so it follows the hints above the buttons. Peers' hasFile flag
is optional, as older builds do not send it.

* fix(collab): send a canvas's cursor and selection to its tab's room again

Canvases read the editor through a proxy that follows the active tab,
and the room lookup by store never matched it, so pointer moves and
selections stopped reaching the room: collaborators lost each other's
cursors, selections, and page markers. The canvas now looks up its
room by its tab's own store, and its selection listener ends when the
canvas unmounts instead of piling up across tab switches.

* feat(collab): one avatar stack for the toolbar, the mobile pill, and pages

The toolbar, the page list, and the mobile HUD each drew the people in a
room their own way, and the mobile pill read 'Online: 3' in hard-coded
English beside a status dot too small to render. AvatarStack now draws
people overlapping with their agent counts and '+N', and every place
uses it: the toolbar wraps each avatar in its menu or hover card, the
mobile pill shows the room's state dot and the stack with a translated
name, and hovering a page with people on it opens a card with the stack
and who is there, with their agents, to follow. The mobile list is the
shared presence list, so it is translated and can follow agents too.

* docs: note the page hover card and mobile avatars in the changelog

* fix(collab): stop listening to a room once its tab leaves it

A session left its Yjs observers and its awareness listener attached
after dispose, relying on destroy() and the order of teardown not to
touch the tab again. It now removes them explicitly and clears its peer
list. Also fix a missing comma in the Polish collaboration guide.
2026-10-06 10:40:25 +00:00
Danila Poyarkov 37bc706a59
fix: lay out fill text and repeat() grid tracks (#866)
* fix(core): let fill text share an auto-layout row

Fill frames grow and shrink from a zero flex basis, so fill siblings split a row's free space. Fill text only got flexGrow, and its measure function capped it at its stored width, 100px for new text, so seven fill labels in a 280px row each kept 100px and overflowed. Without a measurer, the fallback pinned that width and set no grow at all. Fill text now uses the same zero basis as fill frames on both paths.

* fix(design-jsx): read repeat() and minmax() in grid tracks

Track lists were split on whitespace, so columns="repeat(7, 1fr)" became the tracks repeat(7, and 1fr), read as fixed 0px and 1px columns that collapsed the grid. Tokens inside parentheses now stay together, repeat() expands its tracks, minmax() grows like its maximum, and a track the grid cannot express sizes to its content instead of to 0.

* refactor(scene-graph): parse CSS grid tracks with postcss-value-parser

design-jsx read repeat() and minmax() with a hand-written tokenizer and regexes, while dom-css already parses CSS values with postcss-value-parser. Track lists are now parsed in @open-pencil/scene-graph/css on that library, where both packages can use it, and design-jsx calls it. dom-css's hand-written declaration of the library's types is replaced by the types the library ships, which the shared module needs. The Scene Graph guide records that CSS values are parsed there.
2026-10-05 14:06:07 +00:00
Danila Poyarkov 510cdbc36f
feat(scene-graph)!: let a SceneGraph take its ID generator (#887)
* feat(scene-graph): let a SceneGraph take its ID generator

The constructor accepts an ID generator, used for the root node and for
nodes, variables, and collections created later; generated IDs skip any
node, variable, collection, or mode ID already in the graph.

Refs #770

Signed-off-by: Marc Went <marc@went.io>

* perf(scene-graph): check mode IDs without allocating per created entity

Generating an ID spread every collection into a new array and scanned it
for each candidate, which runs on every createNode. Check the node,
variable, and collection maps first and walk the modes with an early exit.
An index of mode IDs is not kept because history snapshots, transfer, and
undo replace collection maps and edit modes in place, which would leave it
stale.

* test(scene-graph): cover injected ID generators and collision skipping

* docs: note the SceneGraph ID generator in the changelog

* test(core): keep reopened .fig GUIDs when a sibling is inserted before them

Refs #770

* test(collab): cover concurrent additions, same-property writes, and delete versus edit

Move the synced-store harness to tests/helpers/collab so the new cases
live in tests/app/collab, and let it bind graph events as a collab session
does. The cases edit both peers while disconnected and check the
converged graphs.

Refs #770

* fix(scene-graph): keep a collection's IDs apart and stop on an exhausted generator

createCollection asks for the collection and default mode IDs before
registering either, so a generator repeating a candidate gave both the
same ID. A generator that only returned taken IDs looped forever, even in
the constructor; it now throws after a bounded number of attempts.

* test: require the inserted GUID and a real disconnect in new tests

The GUID test passed even if export dropped the inserted layer, and the
concurrent-edit helper would have tested sequential sync had its peers
not been disconnectable.

* feat(scene-graph): create variable modes through the graph's ID generator

The editor minted added and duplicated modes as mode:<random hex>, so an
injected generator governed every graph entity except modes added after
a collection's first. SceneGraph.createMode mints the ID and adds the
mode; undo and redo replay it with addMode.

* test(core): type the reopened .fig buffer in the GUID test

* refactor(scene-graph)!: share random helpers and one component property ID

randomHex, randomInt, and randomIndex move from @open-pencil/core/random
to @open-pencil/scene-graph/random, so format packages can use them:
design-jsx and the MCP test server dropped private copies. Component
property IDs, written as prop:<random hex> in six places across Core and
design-jsx, come from createComponentPropertyId.

* docs: point the randomness rule at the shared ID and random helpers

---------

Signed-off-by: Marc Went <marc@went.io>
Co-authored-by: Marc Went <marc@went.io>
2026-10-05 08:21:22 +00:00
Danila Poyarkov 69dba7002f
refactor: define OpenPencil plugin data in one typed registry (#878)
* fix: validate parsed JSON at untrusted boundaries with Valibot

Clipboard HTML, library revisions from shared storage, MCP and automation
WebSocket messages, the MCP discovery file, sidecar output and AI/MCP tool
arguments were JSON.parse'd and cast to their expected types, so a
malformed payload reached the document or crashed paste. They now go
through v.pipe(v.string(), v.parseJson(), Schema), which reports bad JSON
and a wrong shape as the same validation failure.

The path_set tool rejects an invalid VectorNetwork and shares its parser
with create_vector. The CLI library catalog validates its files and runs
revisions through the same size, identity and content-hash checks as the
app; reading image bytes as index-keyed records also stops them coming
back empty. Hand-rolled typeof readers for plugin data, document metadata,
caches and preferences become schemas with their behaviour preserved, and
readCacheJSON takes a schema for its payload.

open-pencil/no-unvalidated-json-parse rejects type assertions on
JSON.parse results other than `as unknown` in src and packages/*/src.

* refactor: validate parsed JSON in tests and tooling

Extend open-pencil/no-unvalidated-json-parse beyond source: tests, helpers and repo tooling now parse JSON through Valibot schemas instead of asserting a type. The shared fixture reader returns a validated object; its old array annotation never matched the fixtures.

* refactor: define OpenPencil plugin data in one typed registry

Every plugin-data key OpenPencil writes is now a field of OPEN_PENCIL_PLUGIN_DATA in scene-graph, with the Valibot schema that reads it; readPluginData and withPluginData replace per-key constants, JSON.parse and hand-matched pluginId/key filters across fig, core, and vue. Moving OkHCL onto it fixes picking a colour rewriting the layer's other plugin data as OkHCL entries.
2026-10-04 17:32:18 +00:00
Danila Poyarkov daef57d52d
build: update dependencies (#873)
* build: update dependencies

Update the AI SDK providers, Vue, Reka UI, Valibot, Zod, es-toolkit,
CodeMirror, Storybook, Playwright, Hono and other dependencies to their
current releases, consistently across workspaces.

The Tauri plugin packages must match their Rust crates, and the new plugin
crates require Tauri 2.12, so Cargo.lock, @tauri-apps/api and the Tauri CLI
move to 2.12 as well.

* build(harness): update the AI SDK harness packages

@ai-sdk/harness 1.0.74 pinned ai 7.0.67, so the workspace carried a second
copy of ai next to the root one; 1.0.138 depends on the same ai release.

The Pi adapter no longer takes a model: HarnessAgent does. The settings
were spread from untyped records, so the compiler could not reject the
stale key and the chosen model would have been dropped; they are plain
literals now.

PiAuthOptions is now PiAuthenticationMode, and auth accepts an environment
record. Pass the gateway key that way instead of writing it into the
process-wide environment while a session is created. Derive the thinking
level from the adapter's settings, which adds 'max'.

* build: hold vue-tsc at 3.3.11

vue-tsc 3.3.12 no longer sees a v-slot binding inside a component that
also has an event listener, so check:vue reports "Cannot find name
'control'" in MCPConnectionEditor and ProfileEditor. 3.3.11 checks them
cleanly.

* fix(ai): keep retryability for provider errors reported mid-stream

From ai 7.0.80 a provider error after the response stream starts is a StreamProviderError rather than an APICallError, so classifyAIChatError lost its isRetryable.

* feat(desktop): accept updates only when signed for their version

Tauri CLI 2.12 records the app version in each updater signature, and
updater 2.13 checks it against the version latest.json announces. With
requireSignedVersion it also rejects signatures that carry no version, so a
tampered manifest cannot pair a newer version number with an older, still
validly signed bundle.

Release assembly now fails when a signature does not name the version
being released, instead of shipping one that installed apps would reject.

* docs: note the dependency update's security fixes in the changelog

* feat(ai): recommend the latest models

The provider packages now know Claude Sonnet 5.5 and Opus 5.5 and the GPT-6
series. Make Sonnet 5.5 and GPT-6.1 Sol the defaults, list Opus 5.5, Fable
5.1, GPT-6 Astra and GPT-6 Luna, and replace the two free OpenRouter models
that OpenRouter no longer serves.

* build: align the fig package's valibot with the workspace
2026-10-04 12:48:24 +00:00
Danila Poyarkov 7b9f506f4a
test: migrate Scene Graph tests into the package and gate new tests/engine files (#790)
Scene Graph unit tests move to packages/scene-graph/tests with a package-local assert helper and test type-checking; five Core- and fig-owned tests move to their owners' engine homes. check:test-homes rejects any new test under tests/engine against a reviewed baseline so the migration debt only shrinks.
2026-09-30 04:53:28 +04:00
Danila Poyarkov e532f616ba
refactor!: move shared primitives below dom-css and core (#771)
* refactor!: move shared primitives below dom-css and core

dom-css depended on core for color conversion, base64 helpers, text
direction, and web-font assets, so core could not use dom-css and every
caller special-cased HTML and Tailwind output.

Color conversion and management, base64 helpers, and text/layout
direction now live in scene-graph under `color`, `bytes`, and
`text-direction`. dom-css takes web-font resolution as an injected
`fonts` option and owns the font face types, so it depends only on
scene-graph and core can depend on it.

BREAKING CHANGE: `@open-pencil/core/color` and `@open-pencil/core/bytes`
are removed, and the direction helpers are no longer exported from
`@open-pencil/core/text`; import them from `@open-pencil/scene-graph`
subpaths. `exportHTMLBundle` takes a font resolver in `fonts` instead of
`'assets'`.

* fix(tools): import color parsing from scene-graph in visual bisect

* fix(mcp): declare the scene-graph dependency

MCP imports `@open-pencil/scene-graph/bytes` since base64 helpers moved
there, but only reached scene-graph through core, so isolated installs
and package checks depended on transitive resolution.

* refactor!: use js-base64 directly instead of a base64 wrapper

Base64 helpers had moved into scene-graph only to sit below dom-css,
but they are a thin wrapper over js-base64 and unrelated to the graph;
fig already called js-base64 directly.

Callers use js-base64 and check `isValid` where input comes from outside
(clipboard, imported HTML, tool arguments, the plugin API). A new
`open-pencil/no-hand-rolled-base64` lint rule rejects atob, btoa, and
Buffer Base64 conversions, and AGENTS.md records the convention.

BREAKING CHANGE: `@open-pencil/core/bytes` is removed; use `js-base64`.

* fix(dom-css): keep images with invalid Base64 inline in HTML export

`exportHTMLBundle` accepts documents parsed from outside HTML, and
js-base64 drops characters it cannot decode, so extracting an invalid
image data URL wrote different bytes. Such images now stay inline.
2026-09-26 11:39:11 +04:00
Danila Poyarkov 6cf1748e31 Release v0.15.1 2026-09-18 16:33:46 +03:00
Danila Poyarkov 7bc5c1fe2f Release v0.15.0 2026-09-16 17:57:35 +03:00
Danila Poyarkov c2aec9bacb
fix(packages): make packed exports runtime-safe (#665)
* fix(packages): make packed exports runtime-safe

Make checked-in package manifests truthful for ordinary npm and Bun packing, and verify installed artifacts under both runtimes. Centralize package discovery, artifact inspection, and bounded process execution so CI and release publication share the same contracts.

* ci: build package dependencies before checks

Keep workspace jobs independent of ignored dist output now that public package exports consistently resolve built artifacts.

* ci: preserve source-first engine tests

Keep the broader dependency build for package and repository validation, but retain Core-only setup for engine shards so workspace tests continue exercising source modules.

* ci: build engine shard dependencies

Build the seven workspace packages imported by engine tests in dependency order. This preserves a single module instance per package and keeps each clean CI shard independent of ignored dist output.

* ci: restore established package build boundaries

Keep the original repository and engine job setup, and add installed artifact verification only after the existing package build. Avoid changing which module copies unrelated tests execute.

* fix(packages): preserve Bun source identity in tarballs

Retain source-first workspace resolution and ship complete source trees for Bun conditions. Verify clean installed consumers without overlaying archives, reuse consumer validation before release publication, and repair Bun 1.3.10 private source alias resolution.

* refactor(tooling): reuse package and process utilities

Use pkg-types for manifest I/O and types, tinyexec for subprocess lifecycle, and npm's pack listing for release staging. Preserve archive verification and project release invariants rather than reimplementing package-manager file selection.

* refactor(tooling): resolve workspace roots at CLI boundaries

Discover and validate the nearest workspace once, support explicit roots, and pass roots to reusable checks. Replace subprocess entrypoint dispatch with direct calls and preserve aggregate package diagnostics without adding arbitrary test timeout increases.

* fix(release): enforce publication boundaries

Use root version alignment and shared validated npm output parsing. Enforce the public npm registry policy and test verification-before-publication, mismatched artifacts, and partial retries without registry writes.

* refactor(tooling): validate package responses with Valibot

Express npm pack and manifest identity contracts as schemas, infer parsed output types, and preserve contextual failures and relative-path safety. Document Valibot as the first-party validation convention while retaining Zod at SDK boundaries.

* refactor(tooling): validate manifests at input boundaries

Share Valibot schemas for consumed manifest fields, recursive exports, supported workspace declarations, and npm registry responses. Infer domain types and reject malformed metadata instead of silently skipping it downstream.

* refactor(tooling): group package helpers by ownership

Colocate manifest and workspace contracts, separate npm response parsing from generic JSON handling, and split smoke packing, installation, and runtime checks. Remove the release tarball forwarding shim and consolidate its coverage in package-artifacts. Preserve public tooling exports and CLI commands.
2026-09-09 18:25:22 +03:00
Ed-Key 9b48e47329 feat(canvas): render arrow stroke caps on lines and open vectors
ARROW_LINES and ARROW_EQUILATERAL stroke caps were accepted by the schema
and round-tripped from .fig files, but the renderer mapped every cap to
Butt, so imported arrows lost their heads and arrows could not be drawn
locally.

Add a pure arrow-cap geometry module to scene-graph (open-endpoint
collection with per-vertex cap overrides, weight-scaled head geometry)
and draw heads in the stroke color for LINE nodes and open VECTOR
centerline strokes, including the dashed-centerline path. Fold the head
reach into strokeOverflow and the node picture margin so heads are not
cropped by visual bounds, caches, or culling. Round-trip per-vertex
stroke caps through the .fig vector style override table so one-ended
Figma arrows import correctly. Expose both caps in the stroke inspector
cap picker with localized labels, and cover the renderer with an
arrow-stroke-caps Playwright canvas snapshot.

Claude-Session: https://claude.ai/code/session_012zjEbyLSBYyPpJP4fu1XNB
2026-08-31 21:25:38 +03:00
Danila Poyarkov 1db92c4603 fix(figma-api): align variant combination behavior
- Match the Figma parent and insertion-index API contract\n- Reject duplicate component references before graph mutation\n- Share slash-name variant derivation with editor actions\n- Add structural regression coverage and changelog entry
2026-08-19 20:05:54 +03:00
Danila Poyarkov 42b1fa07f0 Merge origin/master into feat/configurable-snapping-guides 2026-08-18 22:05:51 +03:00
Danila Poyarkov 91e759d7fd feat(editor): add configurable snapping and guides
- Snap vector points, moved layers, and resized edges to geometry, objects, guides, and pixels
- Add persistent snapping preferences with browser and native menu controls
- Normalize canvas and layout guides across Scene Graph and .fig conversion
- Clear transient snapping feedback across interrupted interactions
2026-08-18 19:11:48 +03:00
Danila Poyarkov f3973202eb
chore: refresh compatible dependencies (#545)
- Refresh compatible workspace dependencies and lockfiles.\n- Preserve package exports and documentation compatibility.\n- Update affected compatibility tests.
2026-08-18 17:44:40 +03:00
Danila Poyarkov 751195891f Release v0.14.0 2026-08-10 11:40:44 +03:00
Luca Candela 18899ff13c
feat(editor): add Figma-style frame presets (#418)
Add frame preset creation and resizing with constraint-aware layout, undo/redo, toolbar flyouts, localization, documentation, and regression coverage.
2026-07-26 00:05:57 +03:00
Danila Poyarkov 9498dc203d chore(tools): harden package and dependency checks 2026-07-01 12:56:45 +03:00
Danila Poyarkov 898431e646 feat: split SceneGraph and Pen packages 2026-06-30 10:54:32 +03:00