A session hosted with `bind` 0.0.0.0 (or ::) kept listening on every
interface but put 0.0.0.0 in its edit and view links, URLs, join codes
and shown address, which nobody can connect to. They now name this
machine's local network address of the listener's family (loopback
without one), on the same port.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6DxbDYxuPEA2pJirCLQ
Splay bends away from the centre of the layer's bounds, as the reference
tool's does: on a ring or an L that centre lies outside the shape, so an
inner edge bends towards the hole. Checked side by side on a ring and an
L at Splay 0% and 100%. The module doc still said splay 0 bends along the
edge's normal; it bends towards the core since #1889.
`MAX_SHAPE_PX` now says it is 4M px on the desktop and 1M on the web, and
that a bigger shape gets no core, so a large glass text or vector can bend
differently once zoomed into, in a 2× export, or on the web.
Docs only.
The dispersion window samples the backdrop 18 times a pixel wherever the
curve bends it, even where the move is so short that the channels split
by a sliver of a pixel. Under 1/256 px every step lies within that of
the moved point, so one sample is within one level (of 255) of the window
over any backdrop, the hardest being 1 px columns of black and white or
of white and nothing. Those pixels now take one sample.
A larger split doesn't qualify: at 1/4 px one sample would move red and
blue by 1/8 px, up to 32 levels across a hard edge.
While a press that went down on the canvas is held, the view centre now follows the canvas rect's own move as well as its size change, so a still pointer keeps its page point when the left sidebar or rail appears mid-press.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The previous fix made a local share's `link` follow the code shown, but its
top-level `url`, the session's WebSocket URL, still carried the edit token:
`collab.join {link: url}` from a view-only share joined with edit access and
the edit reached the host. The link layer now hands over the view way in as a
link and its URL (`SessionInfo::view`, a `WayIn`, in place of `view_link`), and
`collab.peers` gives a local share's `link` and `url` from the way in for the
code shown (`Live::invite`). Online shares are unchanged.
Regression test: `code_tests::a_local_view_share_s_link_and_url_only_let_people_look`
joins through the `link` and then the `url` over loopback; both are welcomed as
viewers whose edits never reach the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
The view keeps its centre when the canvas is resized, so the right panel appearing (minimized UI, on the press that selects) shifted the pointer's page point by half the width over the zoom and the move handling took it as a drag. While a button that went down on the canvas is held, a resize now keeps the top-left instead.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
`collab.share {via: "local", access: "view"}` showed the view-only code but
gave the session's own invite link as `link`, and that link edits: whoever
joined through it could change the host's file. A local share's `link`, top
level and in `sessions`, is now the one that goes with the code shown, and it
follows `collab.share {access}`. Online shares keep the owner's own link there,
as before.
Regression test: `code_tests::a_local_view_share_s_link_only_lets_people_look`
joins through that link over loopback and is welcomed as a viewer whose edit
never reaches the host.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LvG6E2VLhC9d5v8VSsf4ng
The session the app hosts for a local network share now has two secrets:
the edit code and a view-only code (same address and port, a 44-bit secret
of its own). Whoever joins with the view code is welcomed as a viewer: they
see every change and presence, the app makes the file read-only for them,
and the session refuses their batches, as a server does for view links.
Share > Local network offers Can edit / Can view before sharing starts (the
edit code then never shows) and while sharing. collab.share {via: "local",
access: "view"} does the same; share in collab.peers lists both codes and
links. There is still no comment code on the local network.
Next::Stop boxes its event: SessionInfo grew past clippy's
large_enum_variant threshold, and it only ever carries a status or an end.
Tests: loopback_tests::a_view_token_lets_people_look_and_not_edit,
code_tests::a_local_view_code_lets_people_look_and_not_edit,
live_share_tests::a_local_share_can_start_for_looking_only, and the local
share tests that asserted edit-only codes. Docs: collab-protocol.md,
collaboration-parity.md.
plan/ is an optional notebook, the calendar exception and the Rust-only line now live in the Rust rules, and the quality gates no longer say to push to main. The Figma iconography ban stays. CLAUDE.md imports AGENTS.md.
A file imported from another app can hold library copies in its library
store with no record of the library they came from. On an instance of
one, the command took the local path and failed in selecting it with
"isn't on a page (it is a library component)", which says nothing about
what to do. It now says the copy has no link to its library and that
enabling the library reaches it.
Found on a real imported file, where copies of the same library set were
cached more than once and only one of them could be linked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
with_pixel_size read only the PNG's header, so a PNG with a whole IHDR and
a cut-off body passed as the one to paste. Since #1724 image.place refuses
it as damaged, and nothing was pasted: the decoded clipboard image, which
paste falls back to for an unreadable PNG, was never tried. The PNG is now
decoded in full before it is accepted, so the paste falls back as it does
for any other unreadable PNG. The zero-size and pixel-count checks run
ahead of that decode, so an image past the limit is refused before it is
decoded.
Raised by CodeRabbit on #1360 after the merge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an instance of a library component the command refused with "open
that file to edit it". The help center describes it opening the file that
holds the main ("Edit main components"), so it now does: the linked
library file opens in a tab, or its open tab comes to the front, on the
main's page with the main selected and zoomed to.
The library keeps the file's layer ids, so the copy's id map
(ImportedComponent::ids) leads from the local copy back to the exact
layer: a nested instance reaches its own main and a variant that variant.
The new tab takes the canvas size of the tab it came from. A tab still at
the default 1200x800 is refitted to the whole page on its first frame,
which would replace the zoom to the main.
A main deleted from the library file since it was published leaves the
file open with a note. A moved library file, a team library on a server
and a path off this computer are refused before anything opens; the last
uses the same check that guards library reads.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)
The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)
Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1885)
The light is a line on the 1-unit bevel added to the backdrop, as the
reference tool's: fully where the edge faces the light, none below 0.35 of
facing it and 0.6 as much on the opposite side, with a faint gradient across
the curve, 5% darker facing the light and lighter opposite. It was a glow
mixed towards white, as deep as `splay` made it, so `light_reach` goes.
`splay` turns the bend from along the edge's normal towards the way away
from the layer's centre, and no longer widens the light: files with a splay
above 0 look different. The direction to the centre is worked out in the
shape's own frame (`own_frame`, shared with the field cache), so cut renders
match the whole one byte for byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass bends towards its core, and rounded rectangles work it out in closed form (#1885)
The backdrop bends towards the glass's core, the shape shrunk by `depth`
and the bevel, instead of along the smoothed edge normal: round a sharp
corner the bend turns smoothly instead of meeting along the diagonal, and a
part thinner than twice that (a capsule, a letter's stroke) bends across
itself towards its own middle, pixels about as deep as any within 2 px. The
fields keep a bend direction beside the normal (an exact feature transform
from the core, box-blurred once), their key includes the core's depth, and
their margin covers the core.
Rectangles, frames, components, instances and sections whose corners aren't
smoothed, under a rotation, flip and even scale, work their glass out in
closed form (`AnalyticRect`): signed distance, the light's normal (corners
rounded to at least the bevel) and the bend direction (corners rounded to
the core) per pixel, in the layer's own frame, with nothing to rasterise,
transform or keep. A zoom frame draws in 40 ms instead of 54 and a resize
frame in 46 instead of 67 (`glass_render_timings`, release, 2×).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: cut renders of glass match with splay turning a dispersed bend (#1885)
Review follow-up: the cut-versus-whole test had splay only on looks without
refraction, so no turned bend reached the dispersion. A look with splay 1 and
the default bend and dispersion now renders the same however it is cut. The
parity assessment records the light on the bevel and splay's new meaning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: a glass too big to keep bends along its normals, and the core's fields take less memory (#1885)
Review follow-up: a shape that doesn't use the closed form and whose whole
window at the zoom, with the core's margin, would pass `MAX_SHAPE_PX`
(a large vector, text or smoothed rectangle, or a deep glass far zoomed in)
bends along its edge normals instead of working out a core, with the plain
margin. The choice depends on the shape and the zoom alone, so every band and
patch of a render agrees, and no render works out a core over more pixels
than a kept shape holds. The core's feature transform keeps `u32` indices
(16 bytes a pixel) and drops its maxima and core before making the
directions.
The edge normals are box-blurred twice again and the bend directions once,
as their docs say: the two were swapped.
The parity assessment records the bend towards the core and the closed form.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)
Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885)
Review follow-up: the light was added to the backdrop's colour and then
clamped to the backdrop's alpha, so with nothing under the glass (an
export of the pane alone) the line on the bevel vanished, where the old
glow raised the alpha and showed. Now the alpha rises as far as the lit
colour needs (`lit`), and stays the backdrop's wherever the light fits
under it, so renders over an opaque backdrop don't change.
Past `depth` + the bevel the rim light, the gradient and the bend are all
0, so those pixels take one sample of the backdrop and skip the light's
direction, facing and splay work, as the old glow's `reach` early-out did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M8: a kept glass shape works out only its bend again when the depth changes, and a core gets a wider margin (#1885)
Review follow-up. The field cache was keyed on the core's exact depth, so
every step of the Depth slider missed it and worked the whole shape out
again over a window twice as wide: coverage, distances, normals, the
feature transform and the blur, every frame, for every shape that isn't a
plain rounded rectangle. Only the bend directions depend on the depth. The
kept fields (`EdgeFields`: coverage, distances, normals) are now keyed
without it, and hold the bend directions for the last two depths asked
for beside them, so a new depth works out only those (`EdgeFields::bends`).
Two glass layers of one shape at different depths each keep theirs.
The margin a core takes was `reach` + 2 px past the plain one, but the
nearest core pixel can be √2 × `reach` away round a sharp corner, past
what that covers once `reach` is over about 91 px. It is now
1.5 × `reach` + 2 px past the plain margin. The cut-through-the-pane test
gets a 60-unit-deep look (a core 122 px in at 2×).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Glass: splay's doc says splay 0 bends towards the core
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)
The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)
Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's light is a line on its bevel, and splay turns the bend towards the centre (#1885)
The light is a line on the 1-unit bevel added to the backdrop, as the
reference tool's: fully where the edge faces the light, none below 0.35 of
facing it and 0.6 as much on the opposite side, with a faint gradient across
the curve, 5% darker facing the light and lighter opposite. It was a glow
mixed towards white, as deep as `splay` made it, so `light_reach` goes.
`splay` turns the bend from along the edge's normal towards the way away
from the layer's centre, and no longer widens the light: files with a splay
above 0 look different. The direction to the centre is worked out in the
shape's own frame (`own_frame`, shared with the field cache), so cut renders
match the whole one byte for byte.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: cut renders of glass match with splay turning a dispersed bend (#1885)
Review follow-up: the cut-versus-whole test had splay only on looks without
refraction, so no turned bend reached the dispersion. A look with splay 1 and
the default bend and dispersion now renders the same however it is cut. The
parity assessment records the light on the bevel and splay's new meaning.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)
Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's light shows over a transparent backdrop, and its flat inside skips the light (#1885)
Review follow-up: the light was added to the backdrop's colour and then
clamped to the backdrop's alpha, so with nothing under the glass (an
export of the pane alone) the line on the bevel vanished, where the old
glow raised the alpha and showed. Now the alpha rises as far as the lit
colour needs (`lit`), and stays the backdrop's wherever the light fits
under it, so renders over an opaque backdrop don't change.
Past `depth` + the bevel the rim light, the gradient and the bend are all
0, so those pixels take one sample of the backdrop and skip the light's
direction, facing and splay work, as the old glow's `reach` early-out did.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M3: glass bends with the reference tool's thick-lens profile and splits its colours over 8 steps (#1885)
The backdrop bends most a 1-unit bevel in from the edge, falls off as the
3.5th power of the way out of the curve to nothing `depth` further in, and
moves a fifth as much on the bevel itself (it fell off as the square of the
way in from the very edge). Dispersion samples each channel at 8 steps along
the bend with a triangular window, red moving furthest and blue least (it was
three samples, blue furthest), as far apart as the reference tool's: at 100%
red and blue move 0.16 of the bend apart (`Glass::DISPERSION_SPREAD`).
`Glass::max_bend` bounds red's furthest step, so the backdrop read around a
pane covers every sample, and the field margin covers the bevel.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's bend stays within `max_bend`, checked on a single bright pixel too (#1885)
Review follow-up: the bend reaches `depth` × `refraction` a bevel in from
the edge, so `Glass::max_bend` drops its 0.999 and bounds it, as a test now
checks for several refractions. A single white pixel on black pins the
dispersion's triangular window, which a ramp and a flat backdrop can't tell
from one centred sample. The parity assessment records the bend, the
dispersion and how they were measured against the reference tool.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: glass's flat inside skips the bend's curve, and full dispersion stays within `max_bend` (#1885)
Review follow-up: `bend_at` returns 0 straight away past `depth` + the
bevel, where the curve and the bevel's easing are both 0, instead of
working out a power of 0 for every pixel of a pane's flat inside. A test
moves one white pixel along a row under every bend of the profile at full
dispersion and checks that no channel ever reads it further out than
`Glass::max_bend`, which sizes the backdrop read around a pane.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M3: glass effects take variables, and their frost stays within 0–100 (#1885)
Glass's light angle, intensity, refraction, depth, dispersion, frost and
splay bind to number variables, on a layer (`variable.bind`,
`effects/<i>/<field>`, `radius` being the frost) and in an effect style
(`style.bind`), and the Design panel's glass fields take a variable like
the shadow and blur fields. A variable reads as the field shows it:
intensity, refraction, dispersion and splay in percent.
A glass's frost stays within 0–100, the reference tool's range: a larger
frost from an older file draws at 100, and the field, a bound variable and
scaling stop there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: a frost from an older file scales and shows as it draws (#1885)
Review follow-up: a frost saved past 100 draws at 100, so scaling a layer
starts from that (150 halved draws at 50, not 75) and the Design panel's
Frost field shows it. The revision row's binding path escapes its pipes, so
the table keeps three columns.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* M3: Dev Mode and .fig export show a frost from an older file as it draws (#1885)
A frost saved before the 0–100 range draws at 100, and the canvas, CSS and
scaling already read it that way, but Dev Mode's Glass row and the .fig
export still read the stored number, so they said 150 where the canvas
shows 100. Both now read the sanitized frost.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Oleksii Sukholita <oleksii.sukholita@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* M1: dragging a gradient handle on the canvas keeps the colour picker open (#1852)
A press on one of the open gradient's handles or stops closed the picker: the canvas took the press
and began the drag, then the picker, drawn later in the same frame, saw a press outside its card and
closed, committing the drag's interaction at once. The handles vanished with the picker and the drag
went on without an undo step of its own. The handles are the picker's controls: while a gradient
handle or stop drags (`handles::gradient_dragging`, the press included) the picker doesn't close on
an outside press, as it already doesn't while the eyedropper samples. A press elsewhere on the
canvas still closes it. Docs: ui-parity.md.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M1: a handle drag released off the canvas ends there (#1852)
Review: a gradient handle drag whose button went up over a panel, over the colour picker or outside
the window stayed stashed, its interaction uncommitted, until the pointer was back on the canvas,
and the next press there moved the handle to wherever it landed; with the picker now held open while
a gradient drag is stashed, it would also have stayed open meanwhile. `handles::end_released_drag`
commits and clears a stashed drag when the button is up, called from the canvas input's off-canvas
branch and when the pointer is over something drawn above the canvas. Test:
`a_gradient_drag_released_off_the_canvas_ends_there` (released over the sidebar and over the picker).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* M1: a handle drag ends on a release over a ruler, and when Escape or Undo closes its gesture (#1852)
The release check ran only where the pointer was not on the canvas, after the
comment pins and ruler guides had their turn; both return early, so a gradient
handle released over a ruler stayed dragged, uncommitted, until the next press.
It now runs first, wherever the pointer is, and the frame it ends a drag on is
the handle's, as the release on the handle itself was.
Escape (the picker closes and commits) or Undo can also close the gesture while
the button is still down. The stashed drag then kept setting the fill every
frame with no gesture open, one undo step a frame; with no gesture open the
drag is now over.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
* fix(ui): About credits table columns can be dragged wider or narrower
The Contributors and Models tables already open fitted to the dialog, the
name column taking the spare width, and scroll sideways only when there is
not room for every column. Their columns were fixed, though: no edge could
be dragged. They are resizable now, down to 28 points (the flexible column
to its minimum). Widening a column grows the table, as in a spreadsheet,
and only then does it scroll sideways; cells clip instead of overlapping.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* About: refit credits tables when what they measure changes
The columns became resizable, so the table keeps its own widths. A longer
name, a changed cell or a resized window left the old widths in place and
clipped text. The table now goes back to its fit when a column measure or
the clip rect width changes, which scrolling never moves.
Adds tests that drag every column edge, change the fit and scroll a
widened table, as CodeRabbit asked for the drag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Tests: a fitted credits table fills the window exactly
The table ends on the window edge, which the layout rounds to whole points,
so the check allows that edge.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
In page host mode a plain Save of a .figcraft the host opened used the document's internal name, so the host got a different file name with saveAs true instead of an update to the file it opened. Services gains host_file(); Save uses it for a .figcraft host file, Save As keeps the document name, and a missing, .json or .fig host file stays a Save As.
The transports, the message types and the origin checks come from the shared crate (storytold/craft-libs#420); FigCraft keeps what is its own: which saved files become the document, the open size limit and the author.
A page that embeds the web build can hand it a file and get every save back instead of a browser download, and hears about unsaved changes: ?host=parent for a same-origin iframe (postMessage, the protocol the GridCraft, PdfCraft, WordCraft and CADCraft bridges use) and data-host="page" on the canvas for a page that loads the module into its own document (non-bubbling CustomEvents on the canvas). The bridge holds a Transport (Parent or Page) with one send; the pure rules are unit-tested; the hosting guide documents both modes.
* perf(engine): replace linear scans that made document-sized inputs quadratic
enum_of re-classified the whole string for every character; widget names,
comment mentions and devcompare's common siblings each searched a growing
Vec; data.fill's export names and codegen's class names retried every
suffix from 2 against a list, cubic in the number of copies of one name;
and a batch rename template had no length cap although it is expanded
again for every match.
* address review: report the pattern in its own length error
* perf(vectordrawable): resume each repeated name's suffix search where it stopped
* address review: continue from a longer stem when the name suffix counter is exhausted
* perf(collab): memoise layer depths in diff and skip the sibling scan for a new child
* address review: bound the depth walk by the document's node count