Repository navigation
Publish To npm Through Trusted Publishing Instead Of A Stored Token - #396
Merged
Merged
Conversation
Replace the NPM_TOKEN repository secret with npm trusted publishing (OIDC). The publish job runs in the main-only `npm` environment, grants id-token: write on both sides of the workflow_call, and pins npm 11.21.1, the first patch with OIDC for `npm dist-tag add`. A retry dispatch publishes without provenance, since npm would sign main's head instead of the recorded archive's commit. The credential strip list drops the npm token and setup-node placeholder handling. The runbook, README and releases page describe the new setup.
Contributor
Author
|
Reopening so the merge workflow's event payload carries the no-changeset label. |
|
Docs preview: removed when this PR closed. |
tommybarvaag
added a commit
that referenced
this pull request
Oct 23, 2026
) ## Problem `scripts/RELEASE.md` told maintainers to set the package's publishing access to "Require two-factor authentication and disallow tokens" once the token was gone. npm's settings page labels that option "Require two-factor authentication and disallow bypass 2fa tokens (recommended)". The owner applied it on 2026-10-23, after #396 moved publishing to trusted publishing, so the runbook also described a finished step as pending. ## Scope This is a one-paragraph correction to the release runbook. The releases page's stale "not active yet" copy is a separate problem and stays out of this PR. ## Change - The authentication section now says to keep publishing access at npm's current label. It also says the setting stops every npm token from publishing and leaves trusted publishing working, which is what npm's settings page states. - The pending instruction at the end of the npm pin paragraph is removed. ## Verification - `pnpm exec oxfmt --check scripts/RELEASE.md` passes. - `git grep` finds no test that reads `RELEASE.md`. - The label was checked against the package's Publishing access settings on npmjs.com, where the option is now selected.
This was referenced Oct 23, 2026
tommybarvaag
added a commit
that referenced
this pull request
Oct 26, 2026
## Problem Designers have no Fuse Input, Textarea, Avatar or Meter in Figma, and none of the leaf and row components: Select trigger, Pagination, Breadcrumb, the Field text parts, Empty, Link, the Calendar day cell, Item, Alert, Accordion, the Sidebar rows and the Navigation menu bar. The kit cannot draw them yet. Every box is a horizontal auto layout that hugs or has a fixed size. Text never fills, wraps or truncates. Nothing can fill its parent, stack vertically, sit at an absolute position, carry an image, rotate, dash its stroke or hide behind a toggle. ## Scope Mostly tooling in the private `@elmeragroup/fuse-figma` package. The `@elmeragroup/fuse` changes are recipe moves out of TSX into runtime-free recipe files that render identical classes, plus one consumer fix: a lone Checkbox or Switch in a vertical `Field.Root` keeps its size in a container that sizes to its content (U7, patch changeset). The owner chose one PR for this batch instead of a stack of about 27, as an exception to one problem per PR for the Figma kit work. Each unit below is one commit, reviewed before it is committed, so the PR can be read commit by commit. Units land in order and this body is updated as they do. | Unit | What | State | | --- | --- | --- | | U1 | Shared test helpers | done | | U2 | One label source, README facts, `.env.example` | done | | U3–U6 | Box sizing and direction in the plugin and the build, text layout, the field corner and `input/50` variables | done | | U7 | Field keeps a lone Checkbox and Switch at their size | done | | U8–U11 | Input, Textarea, Avatar with image fills, Meter with absolute positioning | done | | U12–U13 | Shared facts, slotless parts, BOOLEAN properties, rotation | done | | U14–U18 | Confirm button, Pagination, Breadcrumb, Select trigger, the Field page and its text parts | done | | U19–U22 | Empty with dashed strokes, Calendar day cell, Link, and the variables for Empty and the W4 rows | done | | U23–U27 | Item row, Alert, Accordion item, Sidebar rows, Navigation menu bar parts | done | | U28 | Fixes from the owner's real-file runs | done | | U29–U32 | Instances inside sets (plugin and build), Kbd group, Breadcrumb trail, Pagination row, Alert action, Empty actions | done | | U34–U37 | Menu and popover recipe moves, client-rendered compare, menu variables and rules, Select's open menu | done | | U38–U42 | Popover, Dropdown menu, Input group, Combobox, Command | done | | U43 | Navigation menu popup | done | | U44–U47 | Sidebar panel recipe moves, Sidebar factory parts, Sidebar sections, Sidebar panel | done | | U33 | Avatar group | done | | U48–U49 | Fixes from the owner's real-file Applies: a swapped instance sublayer, side stroke weights | done | | Compare fixes | Fixes from the owner's real-file compares: the compare mount, Select's label, the Combobox popup width, shortcut glyphs, preview instance lengths | done | ## Change - **U1, shared test helpers.** `component-spec.test.ts` had 22 local build helpers, and four compare cell tests each copied a root lookup and a state-attribute list. They move into `test/fixture-sets.ts` and `test/static-markup.ts`, so the next sets reuse them instead of copying them again. - **U2, loose ends.** The Badge, Button, Toggle and Kbd renderers read their label from the kit definition instead of restating it, and the markup check now compares each box's own text with its label, decoding the entities React writes. The README records that Figma refuses to remove the page it shows, fixes a stale code-map row, and points to a new `.env.example` for the sync's token and file key. - **U3, box sizing and direction in the plugin.** A kit box now carries a direction, a fixed, hug or fill sizing on each axis, min and max limits, and whether it clips. The plugin writes them through `layoutSizingHorizontal` and `layoutSizingVertical`, in the order the probes showed a real file accepts: the direction, then the fixed lengths, then the sizing, then the limits. A direction change rewrites the box's fixed lengths and its layers' sizing, since a turn resizes the box and moves a fill to the other axis. The kit check refuses a variant root that fills, a layer that fills a parent that hugs, and a limit on a fixed axis. The build still emits the same 14 sets, in the new shape. - **U4, box sizing and direction in the build.** The build reads them from the compiled CSS: `flex-col` as a column, `justify-between` as space between, a column's default stretch as a fill across it, `100%` as a fill against a definite parent (along a row only for its sole layer, since CSS sizes a `w-full` beside siblings differently from Figma's fill), a root's `100%` against a preview width its definition names, `min-*` and `max-*` as limits (folded into a fixed length as CSS resolves them), `field-sizing: content`, and `overflow: hidden`. `rounded-full` on a box with one fixed side writes half that side, which the corner-clamp probe showed Figma draws as CSS does, so that `TODO.md` item goes. The sizing reads live in a new `box-sizing.ts`. - **U5, text layout.** A text layer now carries its sizing, alignment and truncation. The plugin derives `textAutoResize` from the sizing and writes the auto resize, the sizing, any fixed length, the truncation and `maxLines` in the order the probes showed, and a new box writes its sizing whatever its stored modes predict. The build reads `truncate` (an ellipsis only in a block box, since a flex box clips its text without one), `line-clamp-*`, wrapping in a definite width, a flex box's text as an item that keeps its own width where the box places it, a native control's placement through its box's alignment, per-cell characters, and one text property that two labels share. A label's mapping lives in a new `label-layout.ts`. A read-only probe showed the published labels already hold the left, top, hugging, untruncated layout the plugin now writes. - **U6, two variables.** `radius-field-box` syncs the field box corner per theme (`radius` in internal themes, `radius-field` in external ones) and binds by its code syntax, as the checkbox corner does, and `input/50` is the disabled field box's fill. Input and Textarea bind both. The owner runs `figma:sync` before the next Apply. - **U7, Field keeps a lone Checkbox and Switch at their size.** A vertical `Field.Root` stretched every direct child with `*:w-full`, which beat Checkbox's `size-4` and Switch's `w-[32px]`. In a container that sizes to its content, a label-less checkbox drew 2px wide and a switch 18px. The stretch now skips Checkbox and Switch, as it already skipped `.sr-only`, and stays at one class's specificity. The responsive md row resets the stretch through the same child selector, so Tailwind emits the reset after the stretch. With a `*:` reset, Tailwind's sort order let the stretch win, and the md row shrank a direct checkbox to 0 and a switch to 16px. A patch changeset says what consumers see. - **U8, Input.** The first set on the text-layout factory: placeholder and value in five states, 10 variants at the docs' 384px width. An `<input>` sets its value on one line whatever its classes say, so a native control's label now fills the box and ends in an ellipsis after one line. An invalid Field's `text-error` reaches the input through inheritance from an ancestor layer the kit does not draw. A custom property that only passes a value to parts the box does not have drops instead of staying unmapped, unless it overrides a synced variable. `inputClass` moves into a runtime-free `input-class.ts`. The markup check reads an `<input>`'s label from its `placeholder` or `value` attribute, decoded. One hand-written kit inventory replaces the page, set and preview lists that three end-to-end tests repeated, so a new set adds one row. - **U9, Textarea.** Placeholder and value in five states, 10 variants at 384px, sharing Input's axes, states and invalid text. The box hugs its height above a 64px minimum, and the text wraps from the top left, since a `<textarea>` wraps its value across its content box whatever its `display` says. `textareaClass` moves into a runtime-free `textarea-class.ts`. The compare renders it through `Field.Control`, as the docs do; Chromium's resize grip is a listed expected difference. - **U10, Avatar with image fills.** Initials or the docs portrait in a circle at four px sizes, 8 variants. The kit can now paint an image: the build carries a PNG's bytes and its SHA-1, which a probe showed is Figma's image hash, so Plan matches a held image without storing anything, and Apply stores it and checks the hash. The kit check accepts a PNG with a whole header of at most 4096px a side. The portrait is rasterised once from the docs' SVG to a checked-in 96px PNG that Figma and the compare share. Avatar's inline classes move into four runtime-free `cn` constants (`avatar-class.ts`); a first `tv` recipe brought tailwind-variants into the `avatar` entry and failed `size-limit` (25.4 kB against an 18.7 kB ceiling), so a follow-up commit returns it to 13.2 kB. A flex item whose text is ASCII letters and digits alone, which a browser never wraps, hugs where its box places it. - **U11, Meter with absolute positioning.** Every mode and level the docs show, 16 variants at 256px. The fill is an absolute layer at the bar's start, SCALE across for its share of the bar and STRETCH down, so it keeps its share when a designer stretches the Meter. The plugin writes the positioning before the size, as Figma refuses FILL on an absolute layer, and rewrites an absolute layer's place and size when a box above it resizes in the same run, so one Apply converges. The build measures an absolute part against its containing block after that block's limits, and leaves a share that a min or max bounds unmapped. A part can carry an inline style that cascades above its classes (Base UI writes the fill's width), a block-level root takes the preview width, and the space before the value becomes the box's gap. The paint readers move into `box-paint.ts`. `meterVariants` gains label and value slots. - **U12, shared facts and slotless parts.** W3's leaf sets read plain `hover:` and style a nested link, so the shared state table holds `hover:` at the hover state and omits `[&>a]` and `[&>a:hover]`. React Aria's Calendar cell and Link render no `data-slot`, so a definition can name its root, or a part, by an element selector, which compare measures by and the markup check resolves below the part's parent. The kit's output is unchanged. - **U13, optional parts and rotation.** A part can sit behind a BOOLEAN property linked to its visibility, so a designer shows or hides it from the properties panel. The build refuses an optional part when any compiled rule reads a `data-slot` in its subtree through `:has()`, since hiding a layer cannot restyle its parent; one selector scanner now serves specificity and these reads. A part or icon can carry a rotation read from `rotate`, so an open caret turns 180°. A quarter turn is taken only on a square layer, since Figma lays a turned layer out by its turned box, which CSS does not. - **U14, Confirm button.** Its own set on the Button page: destructive and success, four sizes, four states, armed off and on, 64 variants, with Button's classes merged with `confirmButtonVariants` as the component passes them. A disabled cell draws disarmed, since disabling disarms the component. Button's set is unchanged. - **U15, Pagination.** The page link (current off and on), the previous and next edges, and the ellipsis, 13 variants on a Pagination page. They draw Button's recipe as the component passes it. The link's number takes the host's type, since Button's `icon` size sets none, so it draws at 16px where the edges' copy uses the control type, as Fuse renders it today. - **U16, Breadcrumb.** The link, the current page, the ellipsis and the separator, 6 variants on a Breadcrumb page. Breadcrumb's inline classes move into a `breadcrumbVariants` slot recipe with identical markup. A set can now name its ancestors by their recipe classes: each gets a layer around the root, cascaded with the cell's states, and the root inherits from the nearest. So the parts take the list's text, and Input and Textarea take an invalid Field root's colour, from Fuse's own classes rather than hand copies. A shared fact holds `sm:` and `md:` at the kit's desktop width. - **U17, Select trigger.** Input's field box with a placeholder or value and a caret, closed or open, at both sizes and four states, 32 variants on a Select page; an open cell turns the caret 180°. The trigger, value and caret classes move into a `selectTriggerVariants` slot recipe with identical markup. The builder learns `*:data-[slot=x]:` classes on a child part, `flex: 1` in a hugging box with no main-axis limit, and an inert line clamp. The box mapper moves into `box-mapping.ts`. - **U18, the Field page.** One Field page holds Label and four new sets, 6 variants: the description, the error, the legend and the title. The plugin moves the published Label set onto it with its ids in the existing two-phase Apply and retires the empty Label page; on a file that already holds the kit, the Field page lands after Icons, since the plugin never reorders pages. Field's part classes move into `fieldVariants` slots with identical markup, and `Field.Root`'s props keep only the orientation axis. The markup check now holds each renderer to the ancestors its definition names. - **U19, first half: dashed strokes.** A stroke can carry a dash pattern, read from `border-style: dashed` at the stroke's width with the lengths Chromium draws, and scaled with a part's scale; a solid stroke plans `[]`, so published sets plan nothing. Empty's inline classes move into `emptyVariants` slots with identical markup. Empty's set waits for its density padding variable (U22) and lands after it. - **U22, variables for Empty and the W4 rows.** Three role tints and ten formula variables. Empty's desktop padding, Accordion's panel padding and Alert's media line read density metrics, so they are computed per density mode. The inner corners of Accordion items and the floating Sidebar's rows read a theme radius and a density padding; each is a `Fuse density` variable whose modes alias hidden per-density values in `Fuse tokens`, so one binding follows both. The owner runs `figma:sync` before the next Apply. - **U19, second half: Empty.** Empty's three frames, default, outline and outline-dashed, 3 variants at 512px, with optional icon media and description behind BOOLEAN properties and the desktop padding bound to U22's density variable. The builder learns letter spacing in `em` as a percent, a wrapping label's max width under a literal cap (taken through the box's limits, siblings, gaps and margins), a part's margins drawn as the padding of a frame around it that hides with the part, and `text-wrap` inherited. - **U20, Calendar day cell.** React Aria's day cell, selected off and on in five states, 10 variants on a Calendar page, from `cellVariants` of each state's render props. A `react-aria-element` root kind reads React Aria's disabled and invalid attributes, the root is found by a selector with an `aria-label` prefix, and compare writes onto the static page what React Aria writes from pointer and focus events. `@internationalized/date` joins the private kit package's devDependencies through the catalog. - **U21, Link.** React Aria's Link in six colour variants at normal and bold weight, in rest, hover, focus and disabled, 48 variants on a Link page, composed as `link.tsx` composes it. The root is any `data-rac` element, since a disabled Link renders a `<span>`. `truncate` is left out: a Figma frame cannot draw CSS's narrower-of-text-and-160px box. The host page's type moves into a shared `host-type.ts`. - **A test timeout fix.** The variable-set tests rebuilt the whole set on every lookup, so after U22 grew the set one test hit Vitest's 30s limit on a CI runner. The file now builds the set once. - **U23, Item and Item link.** Item in three variants at three sizes, with and without a description, with icon or image media (36 variants), and the `[a]:` link row in four states (8), on an Item page. A single set with link states would have run to 216 variants. Media and actions are optional behind BOOLEAN properties; the image media's outline is an inside stroke that takes no layout space. Beside a description the actions fill the row's height and centre their text while the media and content top, as CSS draws them. Item's media, footer and inline classes move into runtime-free recipes with identical markup. The builder learns `item-described:` rules, tag rules at any depth, bounded stretching, a `w-fit` text box that fills its column, and colour mixes toward `transparent` through `@elmeragroup/color`. - **U24, Alert.** Five tones with and without a description, 10 variants at 512px on an Alert page, on Item's parts; the action waits for instances. Alert's text pairs the density font size with a unitless line height, so a unitless line height over a bound font size now draws as a Figma percent line height, which a probe showed follows the bound size in every density mode; a literal size keeps pixels. Alert's title type and media line move into `alertVariants` slots with identical markup. - **U25, Accordion item.** Default and card at three radii, closed and open, in the trigger's four states, 48 variants on an Accordion page. An open cell turns the caret 180° and shows the panel, whose top padding and card corner bind the W4 variables. The cascade now resolves a custom property where it is declared, before inheritance, as CSS does, so a box's override of a synced token paints where the box reads it, and a child's override never changes a value it inherits. - **U26, Sidebar rows.** The menu button (96 variants, expanded and collapsed to the icon rail), sub button (16), group label (1) and badge (3) on a Sidebar page. The group label's text adds a `sidebar-foreground/70` role tint to the synced variables. The has-action rows wait for the menu action (W6), and the floating rows for a floating demo. The builder learns `!important`, `[:where(&)]`, a trailing truncating span as the row's label, an absolute root's position and a zero-width border. Compare forces a peer's state, and counts a forced face as unchanged by design only where the preview's theme resolves both faces to the same colours. - **U27, Navigation menu.** The bar trigger and vertical trigger, closed and open in their states, and the bar link and row link, inactive and active, 28 variants on a Navigation menu page. An open bar trigger turns its caret 180°; the vertical trigger's caret points right, as the Root's `side` sets it in the docs. The popup and the row inside it wait for W7. The builder reads Tailwind's `**:` descendant variant, a root's `inherit` with no host, `max-content`, and limits that bind the fixed length's own variable. A label that comes first in a `justify-between` row fills the room after the gap, so it wraps where CSS wraps it. `packages/fuse` only re-exports `navigationMenuVariants` from the catalog. - **U28, fixes from the first real-file run.** On the real file every text the kit fills across drew 0 wide, its characters stacked one per line: the plugin wrote a new text's layout before its characters, an empty text under auto resize HEIGHT is 0 wide, and Figma's FILL never widens a 0-wide text. A new text now gets its characters first, and Plan gives a 0-wide filling text a width before its fill, so one Apply repairs the published layers. The Select trigger, which hugs its width, now packs its value and caret at the start with its gap, which Figma's space-between dropped. A part that draws a focus ring as a spread shadow gets the near-clear fill and clipping a root gets, so the Accordion trigger's ring draws. Compare checks fonts only inside each measured cell, so the Calendar's month heading no longer stops a run on the external themes. - **Fixes from the second real-file run.** The page layout reads every preview instance before it stacks the previews, since an instance takes its main's new size only when read within one plugin run; the placement then converges in one Apply. The spec lists a layer's shadows in the order Figma paints them (the last on top), so a focus ring draws above an outline border or `shadow-xs`; two-shadow layers plan that change once. The open card Accordion's stack draws its trigger on top, as CSS paints a flex item after its block siblings' backgrounds, so the focus ring stays whole above the panel. - **U29, instances inside sets (second batch begins).** The owner asked for open menus, the Popover and the whole Sidebar, which all need a set to hold other sets' instances. A layer can now be an instance of another set's variant, with TEXT, BOOLEAN and swap properties by name, paint, corner and shadow overrides, sizing, exposure and a visibility link. The plugin plans an instance in stages that read what the previous stage left, and waits for the run's operations when the instance is new, swaps, or shows an inner set or icon the run changes. Sizes settle deepest first, since an instance takes its main's new size only when read. No kit set uses instances yet. - **U30, instance parts in the build.** A definition's part can name another kit component and the cell it shows. The build cascades the inner root in place and diffs it against the inner set's variant: text, BOOLEAN and swap differences become property values, paint, corner and shadow differences overrides, the outer layout's sizing the instance's sizing, and anything else stays unmapped, so a composite cannot drift from its parts silently. Presence checks, stacking and alignment see through instances at any depth; the markup check holds each instance to the inner cell it names. The coverage logs, stacking and flow policies move into their own modules. - **U31, Kbd group, Breadcrumb trail, Pagination row.** The first sets built from other sets' instances, each instance's in-place diff empty. `flex-wrap`, a whole-parent `max-width` and `overflow-wrap` drop only where the build proves nothing bounds the box, tracking available width apart from Figma sizing; `space-y` drops where the markup renders one element child, which the definition declares and the markup check confirms. - **U32, Alert's action and Empty's actions.** Button instances behind BOOLEANs that start false (`Action`, `Actions`), so the published variants keep their look. Alert's tones paint the action through fill and label-colour overrides; Empty's centred text reaches its Button labels as a text-alignment override. Height bounds read a variable's range across modes, capped then floored as CSS resolves limits; the cascade expands `white-space` and `text-wrap` into longhands, so the nearer declaration decides wrapping. - **A hidden layer's fill.** On the real file a hidden auto-layout child reads its sizing as FIXED on an axis it fills, keeping the fill in its grow and align, so the hidden action frames planned FILL again after every Apply. The plugin reads a hidden layer's fill from those, and the next Plan is empty. - **U34, menu and popover recipe moves.** Select, Combobox, Dropdown menu and Popover read their popup, row, label, separator and header classes from runtime-free `tv()` recipes, and the recipe catalog re-exports them with the shared menu-row, overlay and Command list shell classes. Rendered classes are unchanged. - **U35, client-rendered compare cells.** Fuse popups portal through an effect, so static markup draws no popup. A renderer can now mount its cells in Chromium from a browser bundle, open them, portal the popup into the cell and settle with no stray focus or highlight; compare measures the popup, or the trigger and popup together for an open composite, forces highlight by `:focus` or `data-highlighted`, and checks a client set's markup on the mounted page. - **U36, menu variables and build rules.** Synced `foreground/10`, `radius-inner/md-pad-sm`, `radius-inner/xl-gap-sm` and `row-indicator-room`. Build rules for what menu popups declare: an absolute end indicator drawn in flow, an auto start margin, full-bleed separators as section frames, a maximum height that clips, `overflow: auto` as clipping, and drops for motion and the available height. Length identities compare exactly as affine expressions. - **U37, Select's open menu.** The Select page gains the menu group label, menu separator, Select item, Select popup and the open Select, with the popup below its trigger. Rows, labels and separators are instances, a filling part's vertical margins draw in a wrapper frame, and the open Select's root does not clip, so the popup's shadow shows. The popup draws at 144px, the width CSS gives it for every docs trigger; a trigger wider than that is a follow-up. - **U38, Popover.** A Popover page with `Popover content` (the surface, the header, and the basic demo's two Fields, each a frame label beside an `Input` instance) and `Popover (open)` (an outline `Button` trigger instance above the content). An instance part can declare facts that hold for its inner root in place, such as the open trigger's `aria-expanded` and `data-popup-open`, which the diff carries as overrides; a fact may only name a state the inner set omits or leaves unmodelled. - **U39, Dropdown menu.** A Dropdown menu page with the item, checkbox item, radio item, sub trigger, popup (basic, checkboxes, radio group, destructive, submenu), sub content and the open menu, and `menu` and `menu inset` kinds on the shared group label. New rules: an optional icon linked to a BOOLEAN beside its swap property; a part after the row's text; an auto margin before the last of several layers drawn as a filling label; a checkbox or radio indicator first in the DOM drawn last in the row; and popups that hug their rows, where the widest row hugs with the popup's content minimum and every other row fills. The open menu draws the basic menu; the submenu form is left out, since its offset follows the row height and no variable holds it. - **U40, Input group.** The forms Combobox and Command render (a caret button, a clear button, a leading search icon) × rest, focus, disabled and invalid, on the Input page. In Fuse the Input group's control classes move to a recipe module and Combobox's trigger classes into its recipe, with the rendered markup unchanged. New build rules: a one-child instance ignores its gap; a growing control drops its width; `max-height: 100%` fills down; `order` sets the layer order; a direct-child tag selector; and a negative margin on a box that paints nothing folds into its padding, through two new synced density variables. A shadow that reaches past a clipping root now gets a coverage row. - **U41, Combobox.** A Combobox page with the item, the empty message, the popup (flat, grouped, empty) and the open Combobox, an Input group instance above its popup; the Input group gains a `caret open` form. Figma cannot turn a layer inside an instance, so the open caret swaps to CaretUp, and the diff accepts that swap only for a glyph pair whose paths a test proves are a half turn apart. An empty container that paints nothing draws in no cell, since Figma keeps an empty hugging frame at its last height. - **U42, Command.** A Command page with the item (none, shortcut or caret trailing × state, its shortcut a Kbd group instance) and the Groups demo's list box; the dialog stays out. A composite can now set TEXT values on instances exposed below the instance it holds, so each list row shows the demo's Alt shortcut; the plugin sets them through exposure and finds the sublayers again after the row's own writes. In place, a one-word label keeps the row set's filling layout where both draw the same. - **U43, Navigation menu popup.** The Basic demo's open popup for Customers and Business, 264 and 296 px wide, each row a Navigation menu row link instance with the popup's inner corner and text colour. New build rules: a grid that puts every item in one column draws as a vertical auto layout spaced by `row-gap`; a list item stacks blocks as a block box does; and a percentage height in a parent that hugs its height hugs (CSS 2.1 §10.5). An instance fact may now name a variant its inner set omits as a composition, and still refuses a pseudo-element, media condition or transition frame. - **U44, Sidebar panel recipe moves.** In Fuse, the desktop Sidebar panel reads its root, gap, container and inner classes from a `sidebarPanelVariants` recipe, and its other parts from `cn` constants in `sidebar-class.ts`. The rail widths move to a runtime-free module that the `@elmeragroup/fuse/sidebar` facade re-exports, and the recipe catalog re-exports all of them for the kit. The mobile Sheet branch, GroupAction and MenuAction stay inline. Rendered classes are unchanged. - **U45, Sidebar factory parts.** Build rules the Sidebar sets need. A border on some sides draws per-side stroke weights that take room on their side, as a CSS border does. A root's `h-full` takes a preview height. A fixed root drops its position as an absolute one does. A part at opacity 0 whose negative margin cancels its height draws in no layer. An instance may sit absolute at the end and centred down its parent, as the menu badge does: the build proves the centre holds at both densities, and the plugin places the instance once and compares its end inset and centre offset on later Plans, which Figma keeps as the instance's text or its parent changes. - **U46, Sidebar sections.** The Sidebar page gains the group (five menu-button rows, expanded and collapsed), the header (the search input), the footer (a large menu-button row), the menu item with its badge, and the sub menu. In the collapsed rail the group label leaves no trace. The sub menu's rows each move 1px left; where every row of a column moves by the same literal amount, the column's paddings take it (9 and 11 instead of 10 and 10), which matches the browser pixel for pixel. - **U47, Sidebar panel.** The whole Sidebar, `sidebar` and `inset`, expanded and collapsed: the rail at 256 × 448 (48 or 66 collapsed) holding the header, a group, the separator and the footer as section instances. Three build rules: - **Clipped overshoot.** Fuse's separator draws full width from an 8px start inset and its column clips the overshoot, so a 100%-wide part with a start margin in a clipping column draws in a wrapper padded at the start. - **Backdrop.** A root with no fill of its own takes the background of the ancestor that paints behind it, as the inset band shows the Provider wrapper's `bg-sidebar`. - **Part facts.** A part can carry facts only its own element holds, as the separator's orientation does. The compare page's slots grow to 464px for the panel. - **U33, Avatar group.** The docs demo's group as three Avatar instances in a row. A negative `space-x` in a flex row maps to a negative item spacing with later avatars on top, as CSS paints them. A weightless every-child class gives each instance its background-coloured ring as an effects override. - **U48, a swapped instance sublayer.** The owner's Apply on the real file stopped at the Input group's caret: "holds no layer icon-only to override". In Figma, a sublayer swapped inside an instance reports the swapped-in component's key, not the key of the layer it stands for, so the plugin lost the caret's CaretDown icon. The plugin now keys each instance sublayer by the main component's layer at the same place. The in-memory fake now behaves as the file did, which fails the old lookup on the Input group and on an existing Alert test that had been passing only through the fake's gap. - **U49, side stroke weights.** The owner's next Apply wrote 297 changes, but the second Plan still listed the Sidebar sub menu's left stroke and the Sidebar's right stroke. In Figma, a 0 written to one side of a node whose stroke weight was never written sets every side to 0. The plugin wrote only the sides that differed, so the side at 1 was lost. A side stroke now writes `strokeWeight` and then all four sides as one change, and the fake spills as the file did. - **Merge of `main`.** `main` moved during the batch (#339, #374, #394), and again before the review (#395, #396, #397, #398, #399, #404). The branch merges it rather than rebasing, so no unit commit is rewritten. Both merges were conflict-free. The variable counts take #339's `card-shadow` token and U6's two variables: 113 in `Fuse tokens`. - **Review before merge.** A whole-PR review (bugs, quality, simplify) found no bug. It did find duplication the batch had introduced, now removed with the kit's spec byte-equal: - the open-popup facts, the menu group, row and check builders, the user-agent display defaults, the affine helpers and two restated lengths each move to one shared module; - the compare self-check's overlay fixtures go, since the real popup sets now prove the same paths; - one `BLOCK_LEVEL` set counts `flow-root`; - the recipe catalog stops re-exporting 15 names nothing reads. The deferred items, including the per-state instance copies that wait for a real-file run, are in `TODO.md` under "Figma component kit". - **Fixes from the owner's real-file compare (2026-10-26).** The owner's export of all 120 previews (3,508 instances) matched the spec's counts. The compare found three kit defects and one compare defect: - **Compare mount.** Base UI 1.9 appends U+2060 to a live region's text on mount and removes it 200 ms later. The compare read `Combobox.Empty` inside that window under load and stopped. The mount now waits until no client slot holds the marker. - **Select's label.** A flex row with no `justify-content` packs at its start (CSS Box Alignment 3 §5.3). The kit kept such a row centered, so "Apple" sat in the middle of Select's 144px row. A fixed or filling row that holds a hugging layer and no filling one now maps to MIN. Only `select-item` changes in the spec. - **Combobox popup width.** Base UI anchors the popup to the `<input>` inside the Input group, not to the group. So the browser draws it at that input's width plus 28px: 374px dense, 366px comfortable. The kit drew 412px. It now draws the dense widths (popup 374, rows 366), derived from Fuse's density metrics. A Figma width can't follow density without a synced variable that bakes in the kit's 384px sample, so the comfortable popup stays 8px wider in Figma, and the README records it. - **Shortcut glyphs.** Roboto has no glyph for ⇧ or ⌘, and Figma draws each as a blank advance, so the Dropdown menu showed `P` for `⇧⌘P`. The kit names the glyphs Inter draws (`glyphFont`). The plugin sets them as Inter ranges on main components and on instances whose TEXT override shows one, and Plan compares the text's segments. Probes G1 to G12 on the real file settled how range writes, property links, overrides and family bindings change a text's fonts, and the fake's rules 50 to 55 follow them. A shortcut a designer types resets to `font-sans`, and the README says to set ⇧ and ⌘ to Inter. - **Preview instance lengths (second real-file run).** After the owner applied the fixes, the second Plan read clean. Still, the comfortable Combobox previews kept the old 404/412 widths. Probes G13 to G19 on the real file showed the cause. An instance laid out in a mode that changes its size holds its literal width and height, and stops following its main's. `resetOverrides` doesn't release it, a swap releases it only where it equals the old main's, and a bound length keeps following its variable unless the binding is lost. Plan now gives every preview instance the instance parts' sizing sequence, moved to `plugin/src/instance-sizing.ts`. Each fixed axis turns FIXED, then takes its length or binding, and the other axis takes its spec sizing. The fake's rule 56 follows the probes. - **Merge of `main` (#408).** Type checks now run `tsrs` on TypeScript 7.1. The merge was conflict-free, and `tsrs` reports nothing in this branch's code. - **Merge of `main` (#403, #405, #406).** These set text line heights and the heading scale from theme roles. They add 12 synced variables and leave the kit's spec byte-identical. Two conflicts, in the README's collection table and the CLI test's counts, were resolved with both sides' counts. ## Verification - U1: the `fuse-figma` tests and type-check, `oxfmt --check`, oxlint on both packages, the full Fuse unit project, `figma:plugin` and `figma:compare --self-check` pass. The touched files run the same 148 cases with the same names, and `spec.json` and `coverage.md` are identical to `main`'s. One review round by two independent reviewers (bugs, and code quality) found nothing. - U2: the same checks pass, and `spec.json` is unchanged. The new label check went red when a definition's label and its markup disagreed, and when a definition showed its label in a cell that has none. Review: the code-quality reviewer found the check would compare HTML-escaped text with plain characters, so a label with `&` or an apostrophe would fail falsely. The text is now decoded, with a test that also catches decoding twice. - U3: the same checks pass. A script mapped `main`'s `spec.json` into the new field names and found all 14 sets equal, and `coverage.md` is unchanged. A test seeds the in-memory fake with the nodes the real file held (probe P0) and a whole-kit Plan writes nothing to them. Seven new fake rules, each tested and dated with its probe, model the sizing projection, the real error strings, resizes and bindings clearing a fill, direction changes and the limits. Review: both reviewers found that an existing layer was planned against its parent's old direction, that a direction change resizes the box under a fixed length, and that a fixed length beside a limit never converges. All three are fixed, each with a test that failed first. - U4: the same checks pass, the 14 sets build byte-identical and `coverage.md` is unchanged. Each new read has a test on a hand-written fixture whose oracle is the CSS rule it follows. Review: the bugs reviewer found that a main-axis `100%` beside siblings is not Figma's fill and that a zero maximum would reach Figma, and the code-quality reviewer asked for the sizing reads in their own module and for limits that cannot be compared to stay unmapped. All four are fixed. - U5: the same checks pass, the 14 sets build equal once the new text fields are mapped, and `coverage.md` is unchanged. Three new fake rules, from the text probes, are tested. Review: the bugs reviewer found that a flex box's `truncate` takes no ellipsis in CSS (a web platform test covers it), that a wrapping label a row centres sits at its own width, that a hugging box with a `max-width` does wrap its text, and that a fixed text length was compared before a new auto resize. The code-quality reviewer asked for a label module with one line-clamp decision, and found that a native control's vertical placement cannot move a label that hugs its height. All six are fixed, each with a test that failed first. - U6: per-theme values computed by hand from `themes.css`, and a fixture whose corner turned unbound when the formula was mutated. The merged tree passes the same checks, and `spec.json` and `coverage.md` are unchanged by the merge. Review found nothing. - U7: two new browser tests failed on the old classes (checkbox 2px, switches 18 and 14px; the md row's checkbox 0px) and pass now. One measures a lone control against the size it draws outside a Field. The other measures a responsive root in a wide field group against a horizontal one. The Field, Checkbox, Switch, data-table-filter and field-frame browser suites pass, as do `package:check` (the standalone `styles.css` at 33,278 of 36,890 gzip bytes) and the checks above. The kit's `spec.json` is unchanged. Review found nothing. - U8: 15 sets and 982 variants build with 0 unmapped, and the 14 earlier sets are unchanged. Static markup of 11 Input and Input group renders has identical classes before and after the move. The Input and Input group browser suites and `package:check` pass. Review: the bugs reviewer found that the custom property drop would hide a local override of a synced variable, and both reviewers found that ancestor declarations skipped inheritance, so an ancestor's `var()` resolved against the root. The code-quality reviewer asked for a label read from own text in some cells (Textarea's value), decoded attribute values, and one kit inventory for the end-to-end tests. All are fixed, each with a test that failed first. - U9: 16 sets and 992 variants build with 0 unmapped, and the 15 earlier sets are unchanged. Static markup of 11 Textarea renders, consumer classes and Input group included, has identical classes before and after the move. The Textarea and Field browser suites and `package:check` pass. Review found nothing. - U10: 17 sets and 1000 variants build with 0 unmapped, and the 16 earlier sets are unchanged. Static markup of 11 Avatar renders has identical classes before and after the move. The Avatar browser suites and `package:check` pass. Review, round 1: both reviewers found that Plan stored an image to learn its hash, so the dry run wrote to the file; a probe then showed the hash is the SHA-1 of the bytes, and the build now carries it. Both also found the image check weaker than what Figma accepts, and the no-wrap rule admitting fullwidth letters, which can wrap. Round 2 found nothing. - U11: 18 sets and 1016 variants build with 0 unmapped, and the 17 earlier sets and their coverage rows are unchanged. Static markup of 102 Meter renders has identical classes before and after. The Meter browser suite and `package:check` pass. Review: both reviewers found that an absolute share was measured against a containing block before its limits, and that SCALE was given to a share a limit capped. The code-quality reviewer found that a parent resize in the same run left the fill for a second Apply, and that `component-spec.ts` kept child lists that disagreed on which layers flow. All are fixed, each with a test that failed first; the build's output is byte-equal before and after the fixes. - U12: `spec.json` and `coverage.md` are byte-identical to U11's. Review: both reviewers found the markup check matched a slotless part's selector anywhere in the tree instead of below its parent, and the code-quality reviewer found icon owners named by key rather than path; the bugs reviewer found the measured selector did not escape a newline. All are fixed, each with a test that failed first. - U13: `spec.json` and `coverage.md` are byte-identical to U12's. Review: the bugs reviewer found that the presence guard checked only the optional part's own slot, not its subtree, and that the fake hid an instance's hidden layer from `children` although the plugin never turns on `skipInvisibleInstanceChildren` (the probe environment does, so the probe record was corrected). The code-quality reviewer asked for one selector scanner, presence read from every compiled rule rather than only rules that start with the class, and the two files that crossed 1,000 lines split. All are fixed, each with a test that failed first. A further probe settled quarter turns, the rotation sign and a BOOLEAN whose default is false. - U14: 19 sets and 1080 variants build with 0 unmapped, and the 18 earlier sets are unchanged. Each of the 64 rendered cells' classes equals the definition's. `size-limit` and `package:check` pass. Review found nothing. - U15: 22 sets and 1093 variants build with 0 unmapped, and the 19 earlier sets are unchanged. Each of the 13 rendered cells' classes equals the definition's. Review: the code-quality reviewer found a test that restated Button's expectations, which is deleted. - U16: 26 sets and 1099 variants build with 0 unmapped, the 22 earlier sets are unchanged, and Breadcrumb's static markup is identical before and after the move. The Breadcrumb browser suite, `size-limit` and `package:check` pass. Review: the code-quality reviewer found the list's text hand-copied into the kit, where a change to Fuse's list would leave it stale; ancestors are now read from their recipes, with the drawn spec byte-identical. - U17: 27 sets and 1131 variants build with 0 unmapped, and the 26 earlier sets and every other coverage row are unchanged. 14 static Select renders are class-identical before and after the move. The Select and SelectField browser suites, `size-limit` and `package:check` pass. Review: both reviewers found that `flex: 1` was dropped in a hugging box whose `min-width` leaves free space; it now stays unmapped there. The code-quality reviewer asked for the box mapper in its own module. A probe showed Figma's PNG export rounds fractional render bounds up, which the compare self-check now matches. - U18: 31 sets and 1137 variants build with 0 unmapped; the earlier sets are unchanged except Label's page. 24 static Field renders are identical before and after the move. A fake file laid out with Label on its own page moves it with its set, variant and component ids in one Apply, and the second Plan is empty. The Field browser suite, the public type tests, `size-limit` and `package:check` pass. Review: both reviewers found that a horizontal root margin, which narrows an auto-width block, was dropped; the bugs reviewer found a block's line centred where CSS puts it at the top. Both are fixed, each with a test that failed first. - U19 first half: `spec.json` and `coverage.md` are byte-identical to U18's; 11 static Empty renders are identical before and after the move; a fixture with a dashed stroke converges in one Apply and the second Plan is empty. Review: both reviewers found that a scaled part kept its dash lengths unscaled; fixed with a test that failed first. - U22: per-theme and per-density values computed by hand from the theme catalog and the density metrics; a fixture binding per new variable; `spec.json` and `coverage.md` unchanged. Review found nothing. - U19 second half: 32 sets and 1140 variants build with 0 unmapped, and the 31 earlier sets are unchanged. Review: the bugs reviewer found a cap that could reach zero, margins not taken off a framed part's cap, and inherited `em` tracking re-read at the child's size; the code-quality reviewer found the cap ignoring a winning `min-width` and siblings' shares, and a variable-backed text max width the kit check did not validate. All are fixed, each with a test that failed first; Empty's spec is unchanged by the fixes. - U20: 33 sets and 1150 variants build with 0 unmapped, and the 32 earlier sets' spec entries are unchanged (14 coverage rows that were missing for Confirm button, Pagination edge and Icon toggle now appear as dropped). `size-limit` and `package:check` pass. Review found nothing. - U21: 34 sets and 1198 variants build with 0 unmapped, and the 33 earlier sets are unchanged. A renderer test holds the kit's classes to the real Link's markup. Review: the code-quality reviewer found Link reading its host type from Pagination's definition; it now has its own module. - U23: 36 sets and 1,242 variants build with 0 unmapped, and the 34 earlier sets and their coverage rows are unchanged. 13 static Item renders are identical before and after the move. The Item browser suite, `size-limit` and `package:check` pass. Two probes on the real file settled the actions' stretch (PF) and an outline that takes no layout space (PS). Review: the code-quality reviewer found the new stretch rule ignored a part's own height, tag rules applied only to direct children, margins read outside the cascade, and a colour mix parsed ad hoc; the bugs reviewer found the outline taking layout space, an uncapped title, and a growing part's room including its siblings. All are fixed, each with a test that failed first. - U24: 37 sets and 1,252 variants build with 0 unmapped, and the 36 earlier sets are unchanged. 27 static Alert renders are identical before and after the move. The Alert browser suite, `size-limit` and `package:check` pass. Review found nothing. - U25: 38 sets and 1,300 variants build with 0 unmapped, and the 37 earlier sets are unchanged. Review, round 1: both reviewers found that inherited values were resolved again against a descendant's overrides (blocking); the code-quality reviewer found a second, cell-wide override check and the markup checker past 1,000 lines. Round 2 confirmed those closed and found two more edges of the same rule, the page's inherited values and an invalid substitution; all are fixed, each with a test that failed first, and the drawn spec did not change. - U26: 42 sets and 1,416 variants build with 0 unmapped, and the 38 earlier sets are unchanged. Static Sidebar markup is identical before and after the move; `size-limit` and `package:check` pass. Review, round 1: both reviewers found that compare's new fallback could hide a missing state (blocking); a negative control now flags 48 focus cells when their ring rule is removed. Both also found edges of the new layout rules (opposing insets, label alignment and caps) and `!important` crossing inheritance. Round 2 confirmed them closed and found one more importance edge, fixed with a test that failed first. A probe on the real file showed that a collapsed row draws only its icon, as the browser does; at `lg` it draws a clipped "…" where Chromium draws a clipped "D". - U27: 46 sets and 1,444 variants build with 0 unmapped, and the 42 earlier sets and their coverage rows are unchanged. `size-limit` and `package:check` pass. A probe on the real file showed the vertical trigger's row lays out as CSS does, except that a filling label in a space-between row lost the 8px gap. Review: the bugs reviewer found the same gap, now drawn by packing the row at its start with the gap. The code-quality reviewer found an invalid inherited weight drawn as Regular, which now stays unmapped. Both fixes have tests that failed first. A third finding, that the builder measures no text and so cannot prove a word fits its room, is a limit of every filling label and is noted in the README. - U28: the owner's sync, Plan and Apply at U27 and `figma:compare` on the export (1,520 internal cells, then each external page) found the four defects; two probes on the real file settled the cause of the first and third. The in-memory fake now models the 0-wide text, and a gate checks that no filling text ends 0 wide after Apply (red for all 252 under the old order). A seeded file with 0-wide texts, holding their characters or empty and unlinked, is repaired in one Apply, and the next Plan is empty. Only the Select trigger's 32 alignments and the Accordion's 12 focused triggers change in the spec. Review: both reviewers found the repair first read the characters a text held before the run, so an empty, unlinked text needed a second Apply; fixed with a test that failed first. - Second real-file run: after U28 the owner's Apply made 332 changes; compare on all four themes (3,040 cells) shows the repaired texts, the Select gap and the closed Accordion ring matching. Three probes settled what remained (instances settle when read, effect order, stacking). The fake models each; a reconcile test reproduces the stale placement first; the spec diff touches only the two-shadow layers' order and three Accordion roots. Review found the lift could fire past a later stacking context and the kit check missed nested fills; the build now decides the lift with the CSS facts and leaves anything it cannot prove unmapped. - U29: probes N1 to N5 and NS on the real file settled nested properties, swaps, overrides and their survival, exposure, instance sizing, and that a write equal to the main drops an override; the fake models each as a numbered rule, and a fixture kit converges through Plan, Apply and an empty Plan, including an in-place edit of the inner set and an inner set shrinking. The spec is unchanged. Review, round 1: both reviewers found properties and overrides planned against the same held state, so an icon swap removed a layer an override held (blocking); round 2 confirmed it closed and found icon recolouring, held fonts and a FIXED length write; all are fixed, each with a test that failed first. - U30: the spec and coverage are unchanged (no set uses instance parts yet); fixture definitions prove each outcome of the in-place diff, with a negative control per mechanism. Review ran three rounds, each finding edges of the same two problems (presence checks and stacking through instances) plus coverage ownership, multi-field reads and sibling identity; all are fixed with a failing-first test per reviewer scenario. With the three-round cap spent, the last fixes were vetted without a fourth round; the units that follow are the first real consumers. - U31: 49 sets and 1,448 variants build with 0 unmapped; the 46 earlier sets are unchanged. Review, round 1: both reviewers found the new drops trusted a Figma hug as proof that no ancestor bounds the row, and that `overflow-wrap` inherits (blocking); round 2 found grids and unread widths, now bounded too. Each fix has a test that failed first. - U32: 49 sets and 1,448 variants with 0 unmapped; only Alert and Empty change, and each equals its U31 spec once the added hidden layer and property are removed. A probe on the real file showed a text-alignment override on an instance's label is kept through the main's edits. Review found a first alignment rule relied on today's label text, limits applied in the wrong order, and wrapping decided by layer order; all are fixed with tests that failed first. - U34: 10 files; the five components' docs demos, rendered open in the browser, give identical `data-slot` and class pairs before and after (a negative control fails), so no changeset. `size-limit` (select +20, combobox +51, dropdown-menu −5, command 0, popover +17 gzip bytes), `package:check` and the five components' browser suites pass; the kit's spec is unchanged. Review found that the catalog lacked the Command list shell, now re-exported. - U35: the self-check renders three overlay fixtures twice, identically, and a negative control flags a missing highlight; every static cell stays at 0.000% and the spec is unchanged. Review: the mount left Base UI's focus and `data-highlighted` on an open Select (blocking), and the self-check's fake export clipped a popup's shadow; both are fixed with a selected-Select fixture and a threshold-0 run that failed first. - U36: every earlier set and `coverage.md` are unchanged; each rule has a builder test that failed first. A probe on the real file showed a hugging column whose children all fill hugs only to its `minWidth`, which the popup sets follow. Review found the length identity check inexact, section frames not carrying their coverage feeds, and a clipping test that could not fail; all are fixed with tests that failed first. - U37: 54 sets and 1,460 variants with 0 unmapped; the 49 earlier sets are unchanged and only the Select previews change. Probes on the real file showed an instance of a root that does not clip exports the popup's whole shadow, the separator wrapper keeps its height in every density, and a corner binding two instance levels deep survives an edit of the main. Review asked for the margin wrapper in its own module, done with no change to the spec. - U38: 56 sets and 1,462 variants with 0 unmapped; the earlier sets are unchanged. A probe on the real file showed an empty TEXT value keeps a filling text at its width and one line's height, which the Popover's inputs rely on. Review found that a fact could replace a state the inner set models, such as Input's `invalid`; that now throws, with tests that failed first. - U39: 63 sets and 1,508 variants with 0 unmapped; the 55 earlier sets are unchanged, and the shared group label's existing variant is byte-equal. Probes on the real file showed one icon instance holds a visibility link and a swap link together, a swap keeps the layer's name, a filling text in an instance set to hug draws at its own width, and an instance that hugs with a `minWidth` fills up to it and hugs past it. Review found the submenu's `Messages` row would wrap at comfortable density (a probe confirmed Figma wraps it 0.2px short), fixed by the minimum, and the label-fill rule also catching a real `justify-between` row, fixed with a test that failed first. - U40: 64 sets and 1,520 variants with 0 unmapped; the earlier sets are unchanged. `size-limit`, `package:check` and the Input group and Combobox browser suites pass, with the markup byte-identical before and after the moves. A probe on the real file showed a bound end padding follows density, a control filling down beside a 1px inside stroke is the row's height less 2, and an instance's stroke and ring overrides survive an edit of the main. Review found the invalid caret's ring clipped 1px at dense; the build now records such a clip in coverage. - U41: 68 sets and 1,535 variants with 0 unmapped; the earlier sets are unchanged, and the Input group's existing 12 variants are byte-equal. Probes on the real file showed Figma keeps an empty hugging frame's last height, refuses a rotation inside an instance, and keeps a popup wider than its open column with the overflow in the instance's export. Review found the swapped caret's classes missing from coverage and the mirror proof reading only one path per icon; both are fixed with tests that failed first. - U42: 70 sets and 1,545 variants with 0 unmapped; the earlier sets are unchanged. Probes on the real file showed TEXT values set through two levels of exposure survive the outer instance's property writes, a variant switch and edits to the mains, and that same-named caps keep their values by place. Review found the list measured narrower in the compare page than the kit draws it, the fake restoring same-named caps onto the first, and font checks for nested values the plan never writes; all are fixed with tests that failed first. - U43: 71 sets and 1,547 variants with 0 unmapped; the earlier sets are unchanged. Each new rule has a negative control: a grid with a template, column flow or a spanning item stays unmapped, and so do a percentage width in a hugging column and a percentage height in a hugging row. The popup draws no plugin node state that earlier probes do not cover. Review found nothing. - U44: 8 files. The static markup of 77 Sidebar renders, the five docs demos plus variant × collapsible × side × open × consumer classes, is byte-identical before and after, and a negative control differs, so there is no changeset. The sidebar entry grows by 249 B, well under its `size-limit` ceiling. `package:check` and the six Sidebar browser suites (73 tests) pass, and the kit's spec is unchanged. Review found nothing. - U45: the spec is byte-equal to U44's, since no set uses the new rules yet. Probes on the real file showed per-side stroke weights in the layout take room on their own side, and an absolute instance at the end and centre keeps its end inset as its own text widens it and its centre offset through density switches, in the main and in outer instances, with no `x`, `y` or constraint overrides. Review found an anchored instance missing from the paint record when nested, and two places where the in-memory Figma fake stopped following its main; all are fixed with tests that failed first. - U46: 76 sets and 1,556 variants with 0 unmapped; the earlier sets are unchanged. The translate fold has six negative controls that stay unmapped (rows that move apart, one row of two, a move down, a bound padding, a padding that would go below 0, a wrapper that paints). The sets write no plugin node state that earlier probes do not cover. Review found nothing. - U47: 77 sets and 1,560 variants with 0 unmapped; the earlier sets are unchanged. Each panel cell's box and pixels match the browser in the self-check. The clipped overshoot has five negative controls and the backdrop one, all staying unmapped or keeping their own fill. Review found nothing. - U33: 78 sets and 1,561 variants with 0 unmapped; the earlier sets are unchanged. A probe on the real file showed ringed instances overlapping under a −8 spacing in a clipping row, each later ring drawn over the earlier avatar. Review found nothing. - U48: the spec is unchanged. Reads of the owner's file showed which key a swapped sublayer reports in each case. A swap made inside an instance reports the swapped-in component's key. A swap made on the main component's own layer keeps that layer's key, and the instance follows it. A reconcile test reproduced the owner's error before the fix and converges after it. Review found one duplicated test, now folded. - U49: the spec is unchanged. Probes on the real file isolated the spill. It happens only on a node whose stroke weight was never written, and an explicit `strokeWeight` write disarms it. Reconcile tests for a new left-edge variant, a new right-edge variant and the owner's file as it stood each failed before the fix and converge in one Apply after it. Review found no bug. The suggested split of `reconcile.test.ts` is a follow-up. - Real-file probes on 2026-10-22 and 2026-10-23, net zero on a scratch page of the test file that was then deleted, ahead of the units that use each Plugin API member: FILL, HUG and their error cases, resize and bindings against FILL, text auto-resize under FILL, truncation and `maxLines`, SPACE_BETWEEN, direction changes, min and max limits, absolute children with SCALE constraints, image fills and their hash, BOOLEAN properties linked to visibility, rotation in auto layout, dashed strokes, the corner clamp, PNG export rounding, letter spacing, a counter-axis fill, strokes outside layout, percent line heights, a clipped label in a collapsed row, the vertical trigger's row, a 0-wide filling text, and a ring on a part. Each result becomes a dated rule in the plugin's in-memory Figma fake. - Review before merge: three review rounds, the first with simplify. After the fixes, `pnpm ci:checks`, `figma:plugin` (78 sets, 1,561 variants, 0 unmapped, spec byte-equal) and `figma:compare --self-check` pass locally on the merged tree. The moved Select-item fill check goes red when the highlight rule is removed from the built CSS. - The owner's real-file compare on 2026-10-26, at 14a624ff: 3,508 cells in 120 previews, none skipped. 1,981 match to the pixel, and 898 more differ by under 0.5%. Expected differences: Figma rounds auto-width labels up to a whole pixel (444 cells, 0.5 to 1.4px) and the Item title's 19.25px line up to 20px (104 cells). Switch differs only in edge anti-aliasing, and the Sidebar sub-button in its 1px translate, which the kit doesn't draw. The four defects are fixed in the commits above. - Compare fixes: each new or changed test went red without its fix. After the mount fix, the full compare on the owner's export ran to the end. Select's spec diff is `select-item` only, and Combobox's is its four sets' widths. The glyph fix changes only `glyphFont`. Its Plan → Apply → Plan tests on the fake end with an empty second Plan for a main component, an overridden instance, the kit applied before and after the rule, and four designer edits. One review round (bugs and code quality) found that a glyph range bound to an Inter-valued variable never converged (both reviewers), and that two size-reset tests had no probe behind them. Both are fixed, after probes G11 and G12. On the merged tree, `figma:plugin` (78 sets, 1,561 variants, 0 unmapped, spec byte-identical to the pre-merge commit) and `figma:compare --self-check` pass locally, and CI passes every check. Locally on macOS, `pnpm ci:checks` failed one docs studio test, "opens a new target with no menu open" in `test/studio/theme-toolbar.browser.test.ts`: an open Type menu covers the Approve button it clicks. It passed locally before the merge, and CI passes it on this tree and on #406. #406 added heading knobs to that Type menu, so the menu probably now reaches the button with macOS's font widths. - The owner's second real-file run, at 56bd97ae: after sync, Plan, Apply and Plan, the second Plan read "The file matches the kit." The compare on that export (3,508 cells, none skipped) shows: - Select's label matches (0–0.01%). - The Dropdown menu draws ⇧⌘P, ⌘B and ⌘K. Its popup is 175px wide against the browser's 174.42. - The dense Combobox matches exactly. - The comfortable Combobox previews stayed at their old widths, which the preview-length fix addresses. - Preview lengths: three reconcile tests reproduce the owner's stale 404 first, plus a bound length the instance lost and an instance that hugs on a fixed axis (N4). Each converges in one Apply with an empty second Plan, and each failed before its fix. One review round (bugs and code quality) found the sizing order and the hugging case (both reviewers), a fake rule that contradicted G13, an unprobed unbind (now G19), and a test file past 1,000 lines. All are fixed. The spec is byte-identical. - On the merge with #408, `pnpm ci:checks` (31 of 31 tasks, `tsrs` in every type check), `figma:plugin` and `figma:compare --self-check` pass locally. - The owner's third real-file run, at cfaccd22 (rebuild, Plan, Apply, Plan, export), with the compare on that export: - The file's 20 comfortable Combobox previews now read 366 and 374 wide, FIXED/HUG, at their comfortabl…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
The publish job authenticated to npm with
NPM_TOKEN, a repository secret. Every workflow run on every branch of this repository can read a repository secret, includingmerge.ymlruns for PRs from branches inside the repo. The token itself can publish from anywhere until it expires. The runbook kept the token only becausenpm dist-tag addcouldn't use OIDC (npm/cli#8547), and npm fixed that in 11.21.0 (npm/cli#10038).Scope
This is release infrastructure for the one published package. The releases page still says publishing is "not active yet" and calls the channels "designed", although
RELEASE_ENABLEDhas beentruesince 2026-09-29. That is a separate problem, so this PR changes only the sentences about the npm credential. Two follow-ups need engine changes inelmeragroup/internal, and the runbook records both. One splits build and publish into separate jobs. The other keeps provenance on a retry.Change
npm, which onlymaincan deploy to. Thereleasejob inmerge.ymlandpublish-release.ymlboth grantid-token: write, and no secret crosses theworkflow_call.npm dist-tag addover OIDC.NPM_CONFIG_PROVENANCE=false. npm signs provenance forGITHUB_SHA, and a retry runs at the head ofmain, not at the commit its recorded archive came from.release-credentials.tsno longer strips the npm token or restores setup-node's placeholder, because the step holds no npm token now. It still strips the GitHub token and the OIDC request pair during the build and gates.scripts/RELEASE.md,README.mdand the releases page describe the trusted-publisher entries, npm's 2-day expiry for unused entries, the retry entry and the remaining gaps.The PR carries
no-changesetand leaves outno-canaryon purpose. Its merge publishes a canary through OIDC, and that publication validates the newmerge.ymltrusted-publisher entry inside npm's 2-day window.Before this merges, a package owner adds a trusted publisher to
@elmeragroup/fuseon npmjs.com, with these settings:elmeragroup/fusemerge.ymlnpmnpm publishallowedWithout that entry, the canary publication after the merge fails.
Verification
pnpm exec vitest run --config test/vitest.config.mjs test/release-workflow.test.mjs test/merge-workflow.test.mjspasses, 29 of 29. Run againstmain's workflow files, the same tests fail 6, one per new expectation.vitest related scripts/release-credentials.ts scripts/release-pack.ts --run --project unitinpackages/fusepasses, 3 of 3.oxfmt --checkpasses on every changed file.lib/utils/oidc.jsstores the exchanged token in thecliconfig layer, which outranks setup-node's.npmrcplaceholder. It turns on provenance only whenprovenancekeeps its default.libnpmpublish/lib/provenance.jstakes the commit fromGITHUB_SHA.pnpm checkand the docs e2ereadiness-copy.test.ts. The machine's load stayed well above the repo's throttle limit. Lint in an unbuilt worktree also reports type-aware errors in files this PR doesn't touch. CI runs all three.canarytag.