Compare the mirrored href against the path-relative current location so the no-op guard actually skips unchanged URLs. Read context ids through get-query-param so repeated keys resolve to the last value. Cover the navigated effect with stubbed-globals tests (write on change, skip when mirrored, strip stale context) and surface enable-link-preview in the configuration flags list.
AI-assisted-by: muse-spark-1.3-contributor
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Rename the whole feature from unfurl to link-preview: handler namespace, template, /link-preview route, :link-preview flag, nginx variable and docs page. Make the flag opt-in by removing it from the default flags. Serve file-thumbnail objects with full public semantics while the flag is on, restrict the route to GET/HEAD, make a present file-id decisive over project/team context, preserve the query string on the human redirect and skip no-op replaceState writes. Cover the new behavior with backend and frontend tests.
AI-assisted-by: muse-spark-1.3-contributor
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Teach agents to verify their changes with ./scripts/ci: module list,
task selection flags, log locations under .ci-logs/, and per-module
workflows (lint-only pass, --fix, --paren-repair, common/ consumer
checks). Register the skill in the skills README, point AGENTS.md at
the new memory, and add the script to the critical-info dev scripts.
AI-assisted-by: omen-alpha
The flow only supported continue mode (existing branch) or standalone
mode (issue + branch). Add a "direct" argument (aliases: "no branch",
"direct commit") that skips both and commits on the current branch as
it is, even when it is a base branch — for small tooling changes the
user wants committed in place.
AI-assisted-by: omen-alpha
* 📚 Add enterprise section
* 📚 Update images
* 📚 Add SSO module section to Enterprise plan user guide
* 📚 Add more FAQs to Enterprise plan user guide
* 📚 Add SSO configuration screenshot to Enterprise plan user guide
* 📚 Replace membership screenshot with webp to fix broken reference
The membership image reference already pointed to .webp while the tracked
file was .png, leaving a broken image. Swap the asset to match.
* 📚 Document how to access the Admin Console
Add a paragraph explaining how to reach the Admin Console and an
accompanying screenshot to the Enterprise plan user guide.
* 📚 Update Admin Console access screenshot
* 📚 Open Enterprise plan images in a new tab on click
Wrap the enterprise screenshots in a target="_blank" link so they open
full size, matching the behavior on other user-guide pages. The
organization hierarchy diagram is left inline.
* 📚 Document identity provider configuration for SSO
* 📚 Refine SSO session, Admin Console docs, and screenshots
* 📚 Split Organizations content out of the Enterprise plan page
* 📚 Fix wording in Enterprise plan FAQ answers
---------
Co-authored-by: miryamgduque <miryam.gonzalez@kaleidos.net>
* 🐛 Fix missing text in legacy SVG board thumbnails
Board thumbnails rendered by frame-imposter used React's
renderToStaticMarkup, a synchronous pass with no live DOM to measure
text against. Text shapes without a persisted position-data value
therefore rendered as nothing, so a cached board thumbnail silently
lost its text until the board was hovered, selected, or the canvas
was zoomed past 130%, all of which bypass the cached thumbnail in
favor of live content.
frame-imposter now provides the same is-render? context the
standalone exporter already sets, so text without position-data
falls back to the synchronous foreignObject renderer instead of
rendering nothing.
Thumbnails cached before this fix stay broken until something
regenerates them, so on each page load, board thumbnails containing
text are opportunistically regenerated once per browser (tracked via
local-storage) so existing files self-heal without requiring an
edit.
AI-assisted-by: claude-sonnet-5
* ♻️ Use a transducer in heal-stale-text-thumbnails
Fixes a PR review comment: the frame filtering/mapping was spread
across four separate rx operators. Collapse it into a single
transducer pass over frame-ids, leaving only one rx/map to perform
the mark-healed side effect and build the update-thumbnail action.
The event batch sent to the telemetry server was encoded as a
fressian+zstd base64 blob. Send it as a plain vector of event maps
instead: the JSON encoder handles UUID and temporal types natively,
the payload becomes inspectable, and the receiver schema coerces
values back to proper types.
The receiver (penpot-telemetry) now accepts both the blob and the
plain vector, so it must be deployed before this backend change.
AI-assisted-by: omen-alpha
Document stroke to path, toolbar flyouts, line and arrow tools,
font previews, page multi-select, and dashboard grid/list view.
Relates to #9145, #10403, #10580, #10691
Add the frontend MCP variables PENPOT_MCP_URI and PENPOT_MCP_URI_WS,
the PENPOT_INTERNAL_RESOLVER variable used by nginx to resolve the
internal service names, and the enable-mcp flag to the configuration
guide. All three were used by the frontend docker image but were
missing from the docs.
Closes#11318
AI-assisted-by: claude-opus-5
Signed-off-by: Shreyash Agare <envisiontechdevelopers@gmail.com>
Set storeDir in every pnpm-workspace.yaml: `.pnpm-store` at the repo
root and `../.pnpm-store` in the ten module workspaces, so all of them
resolve to <repo>/.pnpm-store. pnpm resolves the value against the
workspace root, and nested workspaces do not inherit settings, which
had left the root workspace and the modules on two different stores.
Add scripts/clean-node-modules: removes every workspace node_modules
in one pass (ignores external/ and .opencode/), keeps the shared store
unless --store removes it too.
Verified: every workspace resolves the same store path; reinstalls
after a full clean reuse the cache with zero downloads;
frozen-lockfile installs pass in all 11 workspaces with no lockfile
changes; the frontend storybook suite stays green.
AI-assisted-by: omen-alpha
Drop the nested pnpm-workspace.yaml and pnpm-lock.yaml from
plugins/apps/composable-test-suite. It stays a plain member of the
plugins workspace (apps/** glob); its dependencies already resolve
through plugins/pnpm-lock.yaml, so no lockfile change is needed.
Update the pnpm update procedure memory to the single-workspace-per-
module layout, keep the invariant that members carry no nested
pnpm-workspace.yaml, and drop the now-stale 12-workspace count from
critical-info.
AI-assisted-by: omen-alpha
Replace __dirname with import.meta.dirname in the plugins workspace
vite configs (root paths and the plugins-runtime tsconfigPath) and in
frontend/vite.config.js, dropping the fileURLToPath fallback.
Import vite.config.iife with the explicit .ts extension in the
plugin-api-test-suite headless/tests configs and allow it in
tsconfig.node.json (moduleResolution Bundler + noEmit), matching the
mcp/packages/plugin precedent.
Remove .storybook/vitest.setup.ts and its setupFiles entry;
@storybook/addon-vitest 10.3+ provisions preview annotations
automatically.
These clear the Vite `configLoader: 'native'` warnings ahead of that
loader becoming the default.
AI-assisted-by: omen-alpha
Run `corepack use pnpm@next-12` (resolved to 12.3.4) on every directory
with a package.json: the repo root, the 11 module workspaces, and all
submodules. Every packageManager field now carries the same pinned
version and hash; the root and backend move off 11.20.0.
Fix the composable-test-suite workspace config (esbuild allowBuilds
placeholder left by pnpm 12) so its install passes, and add the missing
packageManager fields to frontend/packages/ui and mcp/packages/plugin,
since corepack only updates existing fields.
Document the canonical update procedure in
.serena/memories/workflow/updating-pnpm.md.
AI-assisted-by: omen-alpha
Add a "PENPOT" section to the board size presets list with sizes for
File thumbnail, Template cover, Plugin icon and Plugin cover, so
designers no longer need to look up these dimensions manually when
creating thumbnails for files, template submissions or plugin
listings.
Closes#11561
AI-assisted-by: claude-sonnet-5
Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
Two changes to the implement-plan flow:
- It now shows the whole picture before touching the repo: which
issue and branch will be created (or the current branch it
continues on), the execution style, and the task checklist — then
waits for the user's confirmation. Flow detection is read-only;
issue and branch creation moved to the execution step.
- New opt-in execution mode: on request ("step by step"), it runs
one task at a time, commits each one with the create-commit skill,
and waits for the user's review before the next task. The default
stays as it was: every task, one commit.
README and the implement-plan command description updated to match.
AI-assisted-by: omen-alpha
.agents is the shared home that opencode, Claude Code (through the
.claude/skills symlink) and Codex all read, so the skills and the
saved plans now live there instead of .opencode:
- .opencode/skills moved to .agents/skills (24 files, no content
changes).
- .opencode/plans moved to .agents/plans; the .gitignore entry
follows, so plans stay untracked.
- .claude/skills symlink retargeted to ../.agents/skills.
- planner, make-a-plan and review-plan updated to the new plans path;
new .agents/README.md documents every skill with when-to-use
examples and links to each SKILL.md.
- workflow/creating-issues memory: create-issue path updated.
opencode discovers .agents/skills natively, so .opencode needs no
reciprocal link.
AI-assisted-by: omen-alpha
Every skill in .opencode/skills now carries a "When to use" section:
triggers in any phrasing plus the matching /command for the flow
skills, one-line triggers for the utility skills, an
explicit-invocation mirror for ste, and the proactive case for
resolve-git-conflicts.
code-review-criteria drops its old usage bullets ("before merging any
PR ...") for the same role pattern as plan-review-criteria: loaded by
the reviewer subagent of the review-code flow, redirect there to
review code. Flow and criteria skills no longer compete for the same
trigger.
AI-assisted-by: omen-alpha
CLAUDE.md points Claude Code to AGENTS.md as the canonical project
instruction file, and .claude/skills symlinks the opencode skills so
both tools discover the same single source of truth. The root
.gitignore keeps ignoring new files under those paths; the tracked
entries are unaffected from now on.
AI-assisted-by: omen-alpha
Every command in .opencode/commands now only switches to the build
agent, injects the user context, and delegates to a same-named skill
that holds the full flow logic — skills are the primary mechanism for
Claude Code and Codex compatibility.
- make-a-plan, implement-plan, resolve-git-conflicts: flow logic
moved to same-named skills.
- review-plan / review-code: orchestration skills delegating to the
renamed criteria skills plan-review-criteria and
code-review-criteria, with redirecting descriptions so there is no
trigger overlap between flow and criteria.
- create-pr: absorbed the open-pr flow as Open mode (validated
branch-to-PR pipeline using scripts/detect-target-branch) plus a
new Update mode (gh pr edit); /open-pr removed.
AI-assisted-by: omen-alpha
Add a `force` input to Bundles Builder, Docker Images Builder and the
admin-console dispatcher to bypass the existing S3/registry skip
checks and overwrite artifacts unconditionally. Propagate it through
_DEVELOP, _STAGING and _TAG (defaulting to false on non-dispatch
triggers, since schedule/push events carry no inputs).
Add a new _ADHOC workflow to build the full image set (bundle + all
docker images + nitrate admin-console) from an arbitrary ref pair,
for one-off builds of feature branches like test-bameda.
Enrich the build/promote step summaries with the built version
(`git describe --tags --always`), a link to the commit and the build
timestamp. Add run-name to all `_`-prefixed workflows so the target
ref (and, where reliable, the commit sha) is visible directly in the
Actions run list.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Adds the hard rule to AGENTS.md and to mem:critical-info: CHANGES.md
is generated from GitHub milestones during the release process; it
must be updated only via the update-changelog skill flow or on
explicit user request.
AI-assisted-by: omen-alpha
/implement-plan now detects the flow from the current branch instead
of always creating an issue: on a base branch it starts standalone
(issue + branch issue-NNNN from HEAD); on a feature branch it
continues on it with no new scaffolding. Arguments override
detection (standalone, continue, no issue, from origin/<base>);
"no issue" on a base branch creates a plan-<slug> branch. Execution
is direct and the closing suggests /review-code or /open-pr.
/open-pr opens the PR for the current task branch: it detects the
base with scripts/detect-target-branch (canonical: develop, staging,
main), validates commits, issue and remote state, and stops with one
message listing everything missing. It never pushes.
AI-assisted-by: omen-alpha
Add the planning/review command suite for opencode and align the
planner skill with it:
- /make-a-plan (build agent): researches the session subject, drafts
the plan with the planner skill, asks open questions, then saves
the final plan to .opencode/plans/.
- /review-plan: reviews a plan with the plan-review skill; on
approval suggests /implement-plan, on request changes /make-a-plan.
- /review-code: reviews a diff, PR, or code change with the
code-review skill; suggests /make-a-plan for substantive findings.
All commands run as the build agent with explicit read-only rules
that expire when the command's work is done, so follow-up requests
need no agent switching. The planner skill no longer writes the plan
file on its own: it saves only when a command or the user explicitly
instructs it, and it suggests /review-plan and /implement-plan as
next steps.
AI-assisted-by: omen-alpha
Emit clipped `<image href>` for fills with a registered media URL instead
of Skia base64 embeds. Register URLs from the frontend and exporter via
store_image_url. Cover rect, path, frame, and text (glyph clip) cases in
SVG export tests.
Closes#11383
* 🐛 Fix deep-harden of host plugin context on load
ses.harden(context) in loadPlugin deep-freezes every host-owned object
and function reachable through the context. The host keeps needing to
modify those across page navigation (listener wrappers, proxies), so a
later property augmentation (e.g. assigning toString) throws
'TypeError: Cannot assign to read only property toString' and kills the
MCP session (penpot/penpot#11001).
Pass the host context through untouched; sandbox isolation stays at the
compartment boundary (hardened sandbox-owned globals + ses.safeReturn).
Regression test: load-plugin-context.spec.ts (no ses mock).
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Add real SES bootstrap to host-context regression test
The previous load-plugin-context.spec.ts had no SES bootstrap, so it
failed on the original code with 'ReferenceError: harden is not defined'
instead of the intended freeze assertion, and passed on the fixed code
merely by avoiding ses.harden.
Now the spec bootstraps real SES (repairIntrinsics + hardenIntrinsics),
adds a control test proving real ses.harden deep-freezes host-owned
functions (Object.isFrozen === true, later toString assignment throws
TypeError - the #11001 crash signature), and keeps the regression test
asserting loadPlugin leaves host functions unfrozen and patchable.
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Add production-order hardening contrast evidence
Proves the initialization-ordering hazard behind #11001 (cf. #8636):
in production, index.ts runs repairIntrinsics only at module load while
hardenIntrinsics runs later in createSandbox. The original loadPlugin
called ses.harden(context) between those steps, freezing the shared
Function.prototype with plain data properties so later override taming
is skipped and any subsequent fn.toString assignment throws TypeError.
Kept in a separate spec file so the full SES bootstrap in
load-plugin-context.spec.ts cannot mask the ordering effect.
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Apply approved lint fix and CHANGELOG entry
Restores the two approved deliverables missing from the previous push:
the prefer-rest-params fix in load-plugin-harden-order.spec.ts
(replacing the deprecated arguments usage) and the plugins-runtime
CHANGELOG entry for the host-context harden fix (#11001).
AI-assisted-by: muse-spark-1.3
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Remove deep-hardening of host plugin context on load
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
* 🐛 Align CHANGELOG and context comment with reviewed evidence
AI-assisted-by: multica-agent
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
* 🔥 Remove SES semantic tests from plugin regression coverage
Drop the tests that only verify SES library semantics rather than Penpot
application behavior:
- Delete load-plugin-harden-order.spec.ts (pure SES initialization-order
evidence, never calls loadPlugin).
- Remove the ses.harden control test and its SES bootstrap setup from
load-plugin-context.spec.ts.
- Remove the #8636 hardening-order contrast test and the now-unused ses
import from load-plugin-real-path.spec.ts.
Keep the application-level regression coverage: the real loadPlugin
initialization path, permission enforcement, host-context isolation and
safeReturn protection. No production code changes.
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
AI-assisted-by: Omen Alpha
---------
Signed-off-by: Junsoo Choi <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: multica-agent <github@multica.ai>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Add content:write permission checks to Design Tokens plugin API
The Design Tokens API (tokens.cljs) had zero permission checks, allowing
any plugin to create, modify, and delete tokens, sets, and themes
regardless of granted permissions.
Add r/check-permission checks to all 22 write operations across:
- token-proxy: name, value, description, duplicate, remove, applyToken
- token-set-proxy: name, active, toggleActive, addToken, duplicate, remove
- token-theme-proxy: group, name, active, toggleActive, addSet, removeSet,
duplicate, remove
- tokens-catalog: addTheme, addSet
Follows the established pattern from comments.cljs, file.cljs, page.cljs.
Closes#11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add permission checks to shape proxy interactions, detach, export, and variants
The shape proxy (shape.cljs) had multiple operations missing permission
checks, plus a cond ordering bug that bypassed the existing content:write
check for text shapes in commit-fills!.
Fix commit-fills! cond ordering: move permission check before the
text-shape branch so text shapes are also protected.
Add content:write permission checks to:
- interaction-proxy: :trigger, :delay, :action setters, :remove method
- shape-proxy: :addInteraction, :removeInteraction, :detach
- shape-proxy: :applyToken, :switchVariant, :combineAsVariants
Add content:read permission check to:
- shape-proxy: :export (read/extraction operation)
Follows the established pattern from :resize, :rotate, :blocked setters.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add library:write permission checks to variant plugin API
The library.cljs variant operations (variant-proxy and
lib-component-proxy) had seven mutating operations that
did not check the library:write permission, allowing
any plugin to create, modify, and delete component
variants regardless of granted permissions.
Add r/check-permission checks to all 7 operations:
- variant-proxy: addVariant, addProperty,
removeProperty, renameProperty
- lib-component-proxy: transformInVariant, addVariant,
setVariantProperty
Follows the established pattern from the :name and
:path setters in the same file.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add content:write permission checks to flow and flex layout plugin API
Add permission checks to prototype flow and flex layout operations
that were missing them, allowing plugins to modify flows and layout
structure without explicit user permission.
Changes:
- page.cljs: Add content:write checks to flow-proxy (name,
startingBoard setters, remove) and page-proxy (createFlow,
removeFlow)
- flex.cljs: Add content:write checks to flex-layout-proxy
(remove, appendChild)
Follows the established pattern from tokens.cljs, shape.cljs,
and library.cljs.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Add user:read permission checks to plugin API
Add permission checks to user identity accessors that were bypassing
the consent model, allowing plugins to access user data regardless
of whether the user granted user:read permission.
Changes:
- api.cljs: Add user:read checks to getCurrentUser and getActiveUsers
- comments.cljs: Add user:read checks to comment-proxy and
comment-thread-proxy owner/user getters
- file.cljs: Add user:read check to file-version-proxy createdBy getter
When user:read permission is not granted:
- getCurrentUser() returns null
- getActiveUsers() returns empty array
- owner/user/createdBy getters return null
Follows the established pattern from other permission checks in the
plugin API.
Relates to #11137
AI-assisted-by: qwen3.7-plus
* 🐛 Fix problem with token API
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* 🐛 Validate plugin UI URLs cannot target Penpot's own domain
The plugin UI iframe combines allow-scripts and allow-same-origin in
its sandbox. While necessary for plugins to use their own cookies and
storage, this creates a risk if a plugin's UI URL resolves to Penpot's
own origin, potentially allowing the iframe to escape sandbox isolation.
Add validateUIUrl() that checks the resolved URL against Penpot's
origin (from penpotPublicURI or location.origin) and throws if they
match. Called in openModal() after prepareUrl() resolves the URL.
Closes#11271
AI-assisted-by: qwen3.7-plus
* 💄 Fix prettier formatting in plugin-manager.spec.ts
Apply prettier formatting to fix format:check failure.
AI-assisted-by: qwen3.7-plus
* 🐛 Fix problem with penpot origin plugins
---------
Co-authored-by: alonso.torres <alonso.torres@kaleidos.net>
* ♻️ Migrate editable-select to DS icon* and design tokens
The dropdown toggle and selected-item tick used the deprecated
app.main.ui.icons component and the SCSS relied on refactor/
placeholders and aliased design tokens.
Replace both icon usages with the DS icon* component (arrow-down
instead of a rotated arrow, tick unchanged, both at DS size "s"),
and rework editable_select.scss to depend only on DS modules:
placeholders inlined, aliases resolved to their terminal --color-*
tokens, sizes/spacing/borders mapped to DS tokens, logical
properties used where they preserve behavior, and per-state colors
driven by component-local CSS custom properties (fed through
currentColor for the icons, since DS icon* strokes default to
currentColor).
AI-assisted-by: claude-sonnet-5
* ♻️ Migrate frame_grid to the DS and unify its options dropdown
Refactors frame_grid.scss off the deprecated refactor/ styling system
onto DS tokens (borders, sizes, spacing, typography), and replaces the
remaining deprecated-icon usages (gap-horizontal, grid-margin) with
the DS icon* component.
Unifies the square/column/row "reset to default" toggle+panel, which
had drifted into two inconsistent, duplicated implementations, into
two shared components (default-options-toggle*, default-options-
dropdown*) built on the existing dropdown component, with proper
menu/menuitem roles and Enter/Space keyboard activation.
Along the way, fixes several regressions the migration surfaced:
- .show-more-options lost its border width when converted to a
CSS-variable-driven color.
- the reset-to-default toggle was enabled while its own options were
disabled instead of the other way around.
- numeric inputs in .height/.gutter/.margin lost their explicit width,
letting them fall back to the browser's intrinsic input width and
overflow the sidebar.
- a leftover CSS override was forcing the docked dropdown-select's
icon to render fully transparent.
Also fixes editable-select's is-selected check, which compared a
stringified option value against a non-stringified current-value and
so could never match, silently hiding the selected-item checkmark.
AI-assisted-by: claude-sonnet-5
* ♻️ Migrate frame_grid's numeric inputs to the DS component
Replaces the deprecated app.main.ui.components.numeric-input with the
DS numeric-input* across .grid-size, .height, .gutter and .margin,
using its built-in :icon/:text-icon slots (via input-field*) instead
of rendering a separate icon element beside the input.
The gutter/margin icons previously rotated 90deg via CSS to indicate
row vs column orientation; DS ships dedicated icon-ids for both axes
(gap-horizontal/gap-vertical, margin-top-bottom/margin-left-right),
so the icon itself now swaps per grid type instead of being rotated.
The height row's "H"/"W" text label is now the matching DS
character-h/character-w icon rather than literal text.
AI-assisted-by: claude-sonnet-5
* ♻️ Flatten unnecessary nesting in frame_grid.scss
CSS Modules already scope every class to this file, so nesting
purely to dodge cross-file name collisions is dead weight now.
Rewrite the nested rules as flat top-level selectors, dropping
ancestor-wrapper prefixes wherever the target class is already
unique in the file and keeping them only where they still do real
work (winning a same-node specificity fight against another
component's own class).
Also drop --show-options-icon-color: it was assigned across every
state but never actually read anywhere, so it did nothing.
AI-assisted-by: claude-sonnet-5
* ✨ Export solid inner and outer strokes to WASM SVG
SkSVGDevice drops save_layer+Clear used for path Outer strokes, so
solid Inner/Outer strokes on closed shapes are expanded to filled
outlines via stroke_to_path. Center and open paths stay on the shared
stroke path.
Closes#11381
* ✨ Export dotted, dashed, and mixed strokes to WASM SVG
PathEffects do not survive SkSVGDevice, so dotted/dashed/mixed strokes
expand via stroke_to_path with the effect kept, producing filled
outline geometry for all alignments including Center.
Closes#11382
* 🐛 Fix WASM SVG stroke export edge cases
Keep expanded stroke outlines in local space under the leaf CTM so
rotated rects/paths are not double-transformed; overlay open-path caps
after dotted expansion; draw frame strokes outside the content clip;
and route per-side rect/frame strokes through the evenodd band path.
Prevent developer tools from being exposed when the MCP server runs in multi-user mode. Keep them available for local devenv usage and document the mode restriction. Add regression coverage for the registration policy.
Closes#11291
AI-assisted-by: gpt-5.6-luna
Co-authored-by: niwinz <843689+niwinz@users.noreply.github.com>
* 🐛 Fix font-family sample not showing for numeric font names
Setting style.fontFamily to a raw, unquoted family name (e.g. "Micro
5") parses it against CSS's <family-name> grammar: a whitespace-
separated sequence of <custom-ident>s. "Micro" tokenizes fine, but a
bare "5" isn't a valid CSS identifier (idents can't start with a
digit) — it tokenizes as a number instead, so the whole property is
invalid CSS and the browser silently drops it. Every other font in
the list happened to avoid this because none of their names have a
token that's purely numeric.
Quote the family name, matching what font-item-preview* (the font
selector's own preview, a few lines down in the same file) already
does, so it's parsed as a CSS string instead of unquoted identifiers.
Also falls back to the live fontsdb entry's family when the
typography record's own :font-family is blank — a font that was
unloaded when a typography's font/variant was last changed can leave
that field nil (the same failure mode remove-nil-style-attrs already
repairs for shape text spans) — and loads the font unconditionally in
the collapsed asset row, matching the expanded editor, since the
optical-offset cache can otherwise skip loading it entirely.
AI-assisted-by: claude-sonnet-5
* 🐛 Fix flaky typography sample position in automated tests
The optical-centering offset for the "Ag" sample (and the font
selector's fallback name label) resolves asynchronously: first paint
is unshifted, then an idle-scheduled Canvas measurement lands and the
sample jumps to its final position. Any test that checks position or
takes a screenshot shortly after paint races that jump — whether it
runs before or after is a timing accident, not a deterministic
outcome, which is exactly the "sometimes a few pixels up, sometimes
down" flakiness QA hit.
use-optical-offset now returns [offset ready?], with both lazily
initialized from the cache so a cache hit needs no async round-trip
at all. sample-text-style hides the glyphs until ready?, so the
sample only ever appears already in its final, correct position
instead of visibly moving there after the fact.
The font selector's own name label uses the same hook but always
shows real text content rather than a decorative sample, so it keeps
the old behavior instead: hiding it would blank out font names while
scrolling, worse than the minor positional nicety it's fixing.
AI-assisted-by: claude-sonnet-5
Add optional skip-onboarding param to create-demo-profile. When true, the demo profile is created with onboarding-viewed and release-notes-viewed set, so it skips the onboarding flow. Default keeps the current behavior. Cover both cases with RPC tests. AI-assisted-by: muse-spark-1.3-contributor
The creating-prs memory described a branch format (issue/... with a
<type>/<short-description> fallback) that does not match actual repo
practice, where issue-driven branches are issue-NNNN. Replace it with
a Branch Naming section: issue-NNNN as primary, descriptive name
without slashes as fallback.
Add the matching public convention to CONTRIBUTING.md under Pull
Requests, which previously defined no branch naming at all.
AI-assisted-by: omen-alpha