* M3: library analytics list how each style and variable is used
A file reports how many of its layers use each of a team library's styles and
variables, and Show usage lists them by a Type choice with their layers and
files. Layers in instances count for their component. Also ignores plugins/target.
* M3: the style analytics test waits for the instance report too
Bound variables with an iOS or Android code syntax are named in generated SwiftUI and Compose in place of their value: paints, stroke weight, sizes, corner radius, gaps, padding, opacity and text. A variable with none for the platform keeps the value.
Choosing Image shows a checkered placeholder and Upload from computer instead of a dialog at once; an image paint shows its asset preview. A file dropped on the open picker replaces the image, keeping the fill mode, crop, position and adjustments (image.replace, one undo step; paths capped as image.place's). The browser's file picker reaches the tab too. Also fixes a race in library_detach_tests.
Gap 10: a file counts the library instances it places, creates, pastes or duplicates and reports them like its detaches. The server counts what a file's totals grow past their highest in the current week and keeps a year of weeks (actions.json). library.usage {days} and Show usage give insertions and detachments over the past 30 days, 90 days or year. New variants are appended; an older server refuses them and Show usage says so.
A request line can carry timeoutMs beside id and method, capped at 600,000 ms; without it the wait stays 60 s and older apps ignore the field. figcraft-cli mcp --attach PORT --timeout SECONDS sends it with every request and reads the answers that much longer. A timeout's message says how long the request waited.
* M11: a widget opens its plugin's panel (plugin API 5)
Gap 6: a widget callback's ui_show opens the plugin's panel and keeps the module loaded; the panel's callbacks get the widget's id and state and redraw it in the same undo step. Older plugins' ui_show from a widget still opens nothing. The Counter example sets its count from a panel.
* M11: open_panel takes its opener as one value
Gap 14: variable.extendCollection and Extend collection on a collection's row in the Variables editor. Extensions follow edits of the collection they extend at once (normalising keeps them in step whenever variables change), extensions extend again, and a loop of extensions from a hostile file comes apart into plain collections.
library.check refused nothing while automation roots confined it: it
counted libraries it never asked about and remembered them as yours. It
now refuses before remembering any. library.updates and library.list
answered a refused check with the team library cached earlier, as if the
server had nothing newer; the refusal now reaches them instead of
falling back like an offline server. The MCP and control protocol docs
say that a root on a network share still reaches that file server.
* M12: an ended vote's results say who voted for each object
* M12: a board keeps earlier votes' results, listed in Results and deleted one at a time
* M12: the vote Results popover is drawn by small helpers, and its stepper, rows and Clear are tested
The roots confined files only: a confined agent could still publish the
open document to any server, host it on the network or join a session.
The engine reaches the network only through the live link, which now
refuses a call made while the roots confine it (hosting, joining,
publishing, sharing, team libraries, plugin directories), and skips the
background team library checks. The MCP and control protocol docs say so.
* M3: team library analytics count detached instances
Detaching an instance of a library component counts it in the file (libraries.detached, saved with it, capped). The background library check reports the counts after its fetch (ReportDetached); the server keeps them per file beside the file's usage report in usage.json (same caps) and answers Detached with them. Show usage and library.usage list detaches per component and per file (detached, detachesKnown). The new request and reply variants are appended, so every older library message keeps its bytes; an older server refuses the new requests, which the app ignores. Gap 10.
* M3: the detach test waits for the instance count too (the two arrive in separate reports)
Adds a Bosnian (Bosanski) catalog in Latin script with ijekavian forms. It covers
every UI string (1,898), command label (629) and menu wording (31). It is
machine-drafted and marked as a draft until a native speaker reviews it.
Bosnian plural rule in i18n::plural: one for 1, 21, 31… (not 11), few for
2–4, 22–24… (not 12–14), other otherwise. Because one covers 21, every form
keeps {n}. Test: bosnian_tags_and_integer_plurals.
Docs: translating.md (conventions and plural rule), localization-parity.md,
README, ROADMAP, mcp.md and the pref.language param doc.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013Tayw548pu8zRqLH3HjGHN
* M12: create the next board shape already connected (#1597)
A selected sticky or shape shows a + beside each side; hovering one previews where the next goes, clicking makes one of the same kind, size and colour there, joined by a connector, and starts typing in it, in one undo step. board.addConnected {id?, side?, edit?} does it from commands.
* M12: the connected-shape handle and command in every full catalog (#1597)
* M12: Shift+Cmd/Ctrl+Enter on a board shape adds the next one already connected (#1597)
A colour variable's value in a mode can now be {"alpha": {"variable", "opacity"}} - that
variable's colour with its alpha replaced - or {"mix": {"from", "to", "t"}}, a per-channel blend
of two colour variables or literals over the stored values (in the document's colour profile),
alpha included. Calculations resolve lazily like aliases, so bound layers, mode switches,
instances and undo follow their sources; they compose, and a loop, or a calculation that reads
more than 32 variables in its collection's modes (other collections in their default modes), is
refused when it is set. Operands are stored by id and read by exact id, as stored references
are. Files keep them; REST, CSS, Penpot and .fig exports write the colour they show in each mode;
deleting a source leaves its readers on the colour they showed; library import and swap
re-point their operands. The Variables panel shows them with their formula and can swap one for
an alias or detach it to its colour.
* M8: opening a file contacts no team library server it names until you check it (#1097)
* M8: translate the team library check strings (#1097)
* M8: the Assets panel library test checks what allows a library, without its own fake link (Sonar duplication and coverage)
variable.import and the variables parameter of file.open and file.import read an explicit path
with the uncapped reader, while the sibling variables.json next to a REST file already went
through read_capped with MAX_VARIABLES_FILE. variables_param now uses that capped read for paths
on native builds; the browser keeps its no-file-system error. Inline objects and JSON text, and
access roots, are unchanged. The check reads the size before the file, so it does not bound a
file that grows or is replaced in between. Primary file reads and image.place stay uncapped.
variable.import's help and docs/mcp.md name the limit.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
Co-Authored-By: GPT-6 Astra <noreply@openai.com>
* M9: comment.list lists only the threads that mention a name (#1415)
comment.list takes mentions (a name, any case): only threads where a message, the first or a reply, @mentions it, by the same parser the thread view highlights with and each message's mentions reports; combined with the other filters.
* M9: comment.people lists mentioned names; comment.next and comment.prompt carry mentions (#1415)
comment.people adds every name a message @mentions to those who wrote or reacted (once each in any case). comment.next takes the mentions filter and returns the thread as comment.list lists it; comment.prompt's bounded comments carry each thread's mentions (at most 32, names cut to 80 characters). One mention helper serves them all; builds on #1585.
* M9: comment.people lists people who wrote in two cases once
The participant list is deduplicated in any case too, not only the names added from mentions, in
one pass (CodeRabbit on #1591).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* M6: FigJam .jam files open like .fig files (#1342)
A .jam is the same archive as a .fig (its canvas header reads fig-jam.), which the .fig reader already took; File > Open and its filter, drag and drop, file.import and the command line now take .jam too, and the home screen shows its thumbnail. FigJam layers come in as the .fig reader makes them (stickies and shapes with text as frames with their generated layers, other FigJam layers as frames or vectors with a note, unknown types left out with a note). Tests use a synthetic fig-jam. canvas and a .jam made from our own export.
* M12: a FigJam .jam file opens as a board (#1342)
The .fig reader marks a canvas with FigJam's fig-jam. header as FileKind::Board, so a .jam opens with the board toolbar, the dot grid and the board panels and saves as a board; a .fig stays a design file, and importing a .jam into an open file leaves its kind alone. Its objects stay ordinary layers (frames and vectors with the existing import notes). Builds on the .jam opening branch (#1586).
Someone who joins with a comment link (or its join code) sees the file live and adds, replies to, reacts to and resolves comment threads, but can't change the design: their app refuses other edits and the server accepts a comment link's batch only when every change in it touches comment threads. Sharing online makes a comment link and code beside the edit and view ones; the Share popover offers Can edit, Can comment and Can view, and the commenter's toolbar shows a Can comment badge with Move, Hand and Comment.
Backward compatible: clients announce the comments word; one that doesn't is welcomed with a comment link as a viewer and gets comment links listed as view links, a level this build doesn't know reads as view, and an older server that can't make a comment link leaves the share without one.
comment.list takes mentions (a name, any case): only threads where a message, the first or a reply, @mentions it, by the same parser the thread view highlights with and each message's mentions reports; combined with the other filters.
Properties were listed by name, so a rename moved one. Components now keep prop_order (the names as they were made; a rename keeps the place) and every listing follows it. Make slot opens a name field under the frame's Slot row; Enter makes the slot, Escape cancels, a taken name keeps the field open.
Session::execute_as records the agent's presence: its name, the file, and the innermost layers its last edit changed or the selection it set, for 8 seconds after its last command. The app shows it as an avatar in the people row and an outline with a name tag on the canvas, in its own colour. Display only: no undo step, never saved, never synced to a live session.
Every message (the first and each reply) gets `mentions`: {name, known,
you} for each @mention, by the rule the thread view highlights with
(figcraft_doc::mentions, now shared with mention_ranges). Names resolve
against the file's people and you; unknown names are kept with
known: false; a@b.com mentions nobody.
With map: true, edit.duplicate also returns map: each original layer's id, descendants included (nested frames, groups, instances and their sublayers), paired with its copy's id, so a batch can edit inside a copy through a step reference instead of inspecting it. Without map the result is unchanged.
Tools that act on a document, and the control methods engine.execute,
document.inspect, node.get and ui.render, take an optional `document`
(an open file's id from doc.list / document.inspect, or its path). The
call acts on that file and the tab in front stays in front, so an
attached agent keeps editing its own file when the user switches tabs.
Every command an MCP client runs acts as the client's name from initialize (clientInfo.title, else .name) with ' (agent)' after it, or 'AI agent' without one, carried to the app as engine.execute {agent}. Its messages, reactions and read state are its own, and its messages are marked (agent: true in the file and comment.list, an Agent label in the thread). The user's name and comments made in the app are unchanged; in live sessions the name and marker travel with the thread.
A missing family the suite's font store has (Google Fonts, Noto) gets a
Download button. The first one asks the user to allow downloading fonts,
then craft-libs' craft-font-store installs pinned, checksum-verified files
into the Craft apps' shared font folder on a thread of its own. The font
scan reads that folder; once installed the text fits the real font.
font.download / font.downloads drive the store as its agent: refused while
downloading is off, never able to turn it on. Desktop only.
* M1: Crop image from the Object menu, the right-click menu and the image fill row (#1464)
* M1: crop mode shows the image dimmed outside the layer and heavier crop marks (#1464)
* M1: the handle overlay draws gradients and crops in their own helpers (#1464)