14299 Commits

Author SHA1 Message Date
Pablo Alba
4a62e9063e
🌐 Add translations for: French
Currently translated at 96.5% (2407 of 2494 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr/
2026-09-15 17:17:42 +00:00
Aryiu
4b04c05a53
🌐 Add translations for: Catalan
Currently translated at 42.3% (1057 of 2494 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/ca/
2026-09-15 17:17:39 +00:00
Hosted Weblate
dd4c92a4e0
🌐 Update translation files
Updated by "Cleanup translation files" hook in Weblate.

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/
2026-09-15 19:12:42 +02:00
Hosted Weblate
b419efea54
🌐 Merge branch 'origin/develop' into Weblate. 2026-09-15 19:11:04 +02:00
Danial Shirali
8177022ffc
🌐 Add translations for: Persian
Currently translated at 99.8% (2429 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fa/
2026-09-15 17:09:26 +00:00
Chulgil Lee
cdda6ae33c 🐛 Fix form select label resetting to its default option (#11664)
select* resets its label to `default-selected` whenever its options
change identity. The access token dialog builds its options vector
inline, so picking an expiration re-rendered the dialog and the label
snapped back to "Never", although the form kept the picked value and
the token was created with it.

form-select* now passes the form value as `default-selected` when it
holds a non-blank string, so the reset lands on the picked option. The
caller's default still applies while the form value is blank.

Closes #11663

AI-assisted-by: claude-opus-5

Signed-off-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-15 18:56:12 +02:00
Chulgil Lee
8a1bb76ef6
🐛 Fix form select label resetting to its default option (#11664)
select* resets its label to `default-selected` whenever its options
change identity. The access token dialog builds its options vector
inline, so picking an expiration re-rendered the dialog and the label
snapped back to "Never", although the form kept the picked value and
the token was created with it.

form-select* now passes the form value as `default-selected` when it
holds a non-blank string, so the reset lands on the picked option. The
caller's default still applies while the form value is blank.

Closes #11663

AI-assisted-by: claude-opus-5

Signed-off-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: chulgil <2044587+chulgil@users.noreply.github.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-15 18:54:58 +02:00
Shreyash
b9e233f497
✨ Add 3x export scale option (#11598)
Adds a 3x entry to the export scale selector in the workspace design
sidebar and in the viewer inspect panel, so @3x assets can be exported
natively without resizing them in external tools.

Signed-off-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-15 18:27:39 +02:00
Andrey Antukh
b660ea9d53 ✨ Route the app by query string with screen key
Move SPA routing out of the URL fragment into the normal query
string. The screen travels in a reserved `screen` key holding
the route name (`?screen=workspace&team-id=…`); every other
param keeps its name. `rt/nav` and `rt/resolve` keep their
signatures.

This deletes the fragment-mirroring URL surgery, simplifies
link-preview (the server sees everything) and nginx (single
path, no SPA fallback rules needed), and migrates OIDC
redirects, email links, e2e helpers and plugin test utils to
the new format.

Legacy `#/…` URLs translate client-side for one Penpot version
(`legacy-routes`, marked TODO(next-version)); non-SPA paths
are untouched.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-15 17:10:54 +02:00
elhombretecla
a91d81c695 🎉 Add link preview metadata for shared links 2026-09-15 17:10:54 +02:00
Alonso Torres
ff88a055fd
🐛 Fix stalled saving states (#11699) 2026-09-15 16:04:00 +02:00
Eva Marco
1c8b0c1844
🐛 Fix show more files button overlap (#11675) 2026-09-15 15:41:15 +02:00
Eva Marco
a91aa0e37d
🐛 Fix permissions for search and replace (#11713) 2026-09-15 15:40:15 +02:00
Andrey Antukh
39ca4c264c
🔥 Remove onboarding A/B test and welcome file creation (#11707)
Drop the onboarding-03 experiment consulted through
external-feature-flag, keeping the false-branch behavior: registration
never requests a welcome file and the workspace never shows the
onboarding modals. Remove the now-unused welcome-file machinery on the
backend (RPC wiring, welcome_file namespace, welcome-file-id prop and
the post-login redirect). Keep the external-feature-flag helper as the
seam for future experiments and note it in mem:frontend/core.

Closes #11705

AI-assisted-by: muse-spark-1.3-contributor
2026-09-15 14:14:32 +02:00
Belén Albeza
91b433985a
🎉 Add context menu for v3 editor (#11677)
* 🎉 Add a context menu to the v3 text editor

Right-clicking while editing a text shape now opens a menu with Cut,
Copy, Paste and Select all, so those actions stop being reachable only
from the keyboard. Cut and Copy are disabled when nothing is selected,
and with no selection the caret first moves to the click position.

The menu is a new `:text` kind of the workspace context menu, so it
inherits the existing positioning, styling and shortcut hints. Its
entries emit events that drive the WASM editor through the same calls
the keyboard path uses, so a menu edit commits exactly like a
keystroke: same undo step, same layer rename.

A secondary click has to leave the text alone until the menu acts on
it. The pointer handlers now ignore it, both the right button and the
macOS Ctrl+Click that reports button 0, so it can no longer start a
drag-selection over what was selected. Pressing the mouse on the menu
would blur the capture surface and end the session, so the menu
cancels the default action of `mousedown` while a text is being
edited.

Paste reads the system clipboard through `navigator.clipboard`, since
a menu click carries no clipboard event.

Closes #10914

AI-assisted-by: claude-opus-5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ✨ Add Playwright tests for the text editor context menu

Eight tests under "Text Editor context menu" in the v3 editor spec.
They load the existing lorem ipsum fixture rather than drawing a shape,
enter edit mode, and drive the menu with a real right click.

They cover the four entries, that the right click keeps the selection
(copy returns the whole text), that a right button drag no longer
replaces it, that moving the caret abandons a staged typography, and
that cut and copy are disabled at a collapsed caret. Typing right after
a menu action also shows the editor kept the focus while the menu was
open.

AI-assisted-by: claude-opus-5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ♻️ Use a test id instead of a DOM id in the v3 editor spec

AI-assisted-by: claude-opus-5
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 12:53:07 +02:00
Alonso Torres
8d69374386
🐛 Makes the CI more stable (#11698) 2026-09-15 10:43:41 +02:00
María Valderrama
66876f16b2
✨ Error page for instances without admin-console configuration (#11649)
* ✨ Error page for instances without admin-console configuration

* 📎 Code review
2026-09-15 07:24:55 +02:00
Alexis Morin
c45bb130bf
🌐 Add translations for: French (Canada)
Currently translated at 100.0% (2432 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-09-14 17:51:41 +00:00
Yaron Shahrabani
ec996f08ac
🌐 Add translations for: Hebrew
Currently translated at 88.6% (2155 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/he/
2026-09-14 17:51:30 +00:00
Andrey Antukh
0caabfdd5b Merge remote-tracking branch 'origin/staging' into develop 2026-09-14 16:36:07 +02:00
Andrey Antukh
5d5f7fcc14 🐛 Fix workspace crash on non-string thumbnail URIs
Thumbnail :uri values arrive from the server over transit, so
media-ids decode to UUID objects instead of strings. The
resolved-uri? helper added in #11563 called
clojure.string/starts-with? on them unconditionally, raising
TypeError: str.lastIndexOf is not a function and crashing the
workspace on frame render. Guard with string? so non-string
URIs fall through to resolve-media, which stringifies them.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-14 16:33:54 +02:00
Andrey Antukh
ba608f8c77 Merge remote-tracking branch 'origin/staging' into develop 2026-09-14 14:50:32 +02:00
Andrey Antukh
5931f60d53
🐛 Fix workspace crash when resolving thumbnail data URIs (#11563)
The workspace-thumbnail-by-id ref unconditionally called resolve-media
on thumbnail URIs, which caused a stack overflow when the URI was a
data URI (which can be megabytes long for large images).

Data URIs contain thousands of '/' characters (base64 uses '/' as one
of its 64 characters), causing lambdaisland.uri/join to iterate
thousands of times in remove-dot-segments and overflow the JavaScript
call stack.

Add a resolved-uri? helper that checks if the URI already starts with
'blob:' or 'data:', and skip resolve-media for those cases. Only call
resolve-media when the URI is a plain UUID (media-id from the server).

Closes #11562

AI-assisted-by: qwen3.7-plus
2026-09-14 13:25:31 +02:00
Andrey Antukh
8629dc6b2c
🔥 Remove unused comment profiles code in the frontend (#11624)
* 🔥 Remove unused comment profiles fan-out in dashboard

The dashboard event retrieve-unread-comment-threads issued one
get-profiles-for-file-comments call per distinct file id and stored
the result in :current-team-comments-users, a state key no one
reads. The dashboard renders each thread author from the thread
payload via get-owner, so the fan-out was N+1 dead work.

Drop the per-file branch and the state write; the event now issues
a single get-unread-comment-threads call.

AI-assisted-by: deepseek-flash

* 🔥 Remove unused file comments users event and state

fetch-file-comments-users had no callers and passed a :team-id
where get-profiles-for-file-comments requires :file-id. Its only
effect was writing :file-comments-users, a viewer state key no
one reads.

Remove the event and the unused state key. The viewer still
loads comment profiles through fetch-profiles.

AI-assisted-by: deepseek-flash
2026-09-14 13:23:44 +02:00
Shreyash
6a46f8beb5
🐛 Fix email already used showing a toast instead of an input error (#11647)
* 🐛 Fix email already used showing a toast instead of an input error

Registering with an email that already exists reported the problem with a
toast while the email input stayed in its valid state, which reads as if the
form was accepted. The message now lands on the email input itself, the same
way the recovery and change password forms report server side errors.

The error is written to :extra-errors because the form mutator recomputes
:errors from the schema on every change, so a value written there is dropped
on the next render.

Fixes #10890

* 📎 Update CHANGES.md to remove email registration fix

Removed a note about a fix for email registration error display.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>

---------
Co-authored-by: Shreyash Agare <agareshreyash26@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-14 10:13:54 +02:00
Alexis Morin
0dbc8e1b55
🌐 Add translations for: French (Canada)
Currently translated at 97.3% (2368 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-09-11 14:51:30 +02:00
Alonso Torres
947954933c
🐛 Fix problem with node splitting in paths (#11553)
* 🐛 Fix problem with node splitting in paths

* 🐛 Fix merge splitting node in paths
2026-09-11 14:20:09 +02:00
Alejandro Alonso
87e5ffdf94
🐛 Restart progressive render on canvas background change (#11643)
Cancel Partial/ViewportReady before invalidating tiles so a mid-flight
pass does not leave background-only holes. Closes #11642.
2026-09-11 14:12:57 +02:00
Eva Marco
f3da8af7b6
🎉 Avoid interacting with clipped content (#11613)
* 🐛 Fix nested board drop target ignoring ancestor clip bounds

Frame hit-testing (get-frame-by-position, get-frames-by-position and
top-nested-frame) only checked a candidate board's own rectangle,
without accounting for an ancestor board with clip content enabled.
A nested board wider/taller than its clipping ancestor could still be
picked as the drop target in its invisible, clipped-away area, so a
dragged shape would get reparented there and disappear from view.

Add clipped-by-ancestor? to reject a point when it falls outside the
bounds of any ancestor board that has clip content enabled, so the
lookup now stops at the correct visible ancestor instead of
descending into the hidden region.

* 🐛 Fix Ctrl+click deep-select reaching into clipped board area

The clip-aware quadtree query (query-index) filters candidate shapes
by whether they overlap every clip-parent ancestor, but the whole
filter was skipped whenever clip-children? was false. That flag is
turned off while a modifier key (Ctrl/Cmd) is held for deep/penetrate
selection, which was meant to let it reach past boolean/mask clip
boundaries, but it also disabled enforcement for board "Clip content"
ancestors, letting a modifier-held click select a shape sitting in a
board's invisible, clipped-away region.

overlaps-parent? now only relaxes the check for non-frame clip-parents
(bool shapes / mask children) when clip-children? is false; board clip
ancestors are always enforced regardless of the modifier key.
2026-09-11 13:42:13 +02:00
Eva Marco
c0221a9bf8
🐛 Hide "Create typography style" button for shapes with missing fonts (#11527)
The button let users convert a text shape's inline styles into a
typography asset even when the shape's font-id couldn't be resolved
(e.g. a custom/team font that was removed or isn't loaded), silently
baking a missing font into the new typography asset.

Guard the button on the font actually resolving via
app.main.fonts/fontsdb, in addition to the existing checks (no
typography or token already applied, single selection).

Added e2e coverage for all four conditions that must independently
hide the button: missing font, applied typography asset, multiple
selection with differing values, and applied typography token.

AI-assisted-by: claude-sonnet-5
2026-09-11 13:41:11 +02:00
Eva Marco
7ff76a9ebc
🐛 Fix project name width in dashboard header (#11564)
The project title's max-width was capped via an inline style computed
from the number of thumbnail columns fitting in the grid below it (an
unrelated value, reused only because it happened to be in scope). This
produced an oversized gap between a short/medium title and the file
count, timestamp, and action buttons, and gave long titles an
arbitrary, columns-based truncation point unrelated to the row's
actual available width.

Replace it with a standard flexbox truncate-to-fit: the title sizes to
its own content and sits right next to the info/actions, only
shrinking (and ellipsizing) once the row runs out of room, while the
info/actions never shrink.
2026-09-11 13:40:58 +02:00
Alonso Torres
fc8c5a98de
🐛 Fix handler change to equal (#11612) 2026-09-11 13:23:45 +02:00
Alonso Torres
b598d7d72e
🐛 Fix problem in plugins api when removing interactions (#11621) 2026-09-11 13:19:55 +02:00
Elena Torró
32ed9b5a08
🐛 Fix board clip on drag (#11620) 2026-09-11 08:27:11 +02:00
Elena Torró
0913545b41
🐛 Fix letter spacing and position when flattening text to path (#11555)
* 🐛 Fix letter spacing and position when flattening text to path

* ♻️ Simplify text to path conversion and add flatten tests
2026-09-11 08:20:22 +02:00
Andrey Antukh
37dab75e1a Merge remote-tracking branch 'origin/staging' into develop 2026-09-10 20:29:44 +02:00
makesomethingshit
99c036feac
🐛 Close nitrate modal when navigating to current plan (#11615)
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-10 18:17:11 +02:00
Andrey Antukh
7c27ed812a
♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section (#11531)
* ♻️ Consolidate HIGHLIGHTS.md into CHANGES.md 🚀 section

Eliminate the redundant HIGHLIGHTS.md file and make CHANGES.md
the single source of truth for version highlights.

- Add 🚀 section for 2.15.0 (MCP server integration)
- Add 4 missing highlight entries to 2.17.0 🚀 section
- Rewrite frontend parser to extract from CHANGES.md 🚀
  subsections instead of flat HIGHLIGHTS.md format
- Decouple parse-latest-released-version from highlights
  extraction so it works independently of 🚀 content
- Conditionally render highlights section in modal when non-empty
- Rewrite tests for new parser behavior (11 tests, 21 assertions)
- Delete HIGHLIGHTS.md and remove .gitignore exception
- Add step 8b to update-changelog skill for proactively
  proposing highlights during release workflows
- Add missing-highlights and missing-highlight-reference
  anomaly types to the changelog anomaly report script

Closes #11530

AI-assisted-by: qwen3.7-plus

* ♻️ Use consistent string library and add multi-version test

Address code review findings:

- Use str/split (cuerdas) consistently in extract-rocket-items
  instead of mixing cstr/split (clojure.string)
- Add parse-highlights-extracts-multiple-versions test to verify
  the parser correctly extracts 🚀 items from multiple
  versions in a single CHANGES.md body

AI-assisted-by: qwen3.7-plus

* ♻️ Scope 🚀 checks to X.Y.0 and split gaps from anomalies

Type C now only checks released X.Y.0 versions, since patches never carry 🚀 subsections by design. Type D requires both issue AND PR references with exact format, accepting multi-PR entries. C/D are reported as highlight gaps in their own section and no longer count toward the anomaly total. Key Principles and anomaly definitions updated to match. Addresses review comments on PR #11531.

AI-assisted-by: muse-spark-1.3-contributor

* ✨ Render markdown links and bold in check-updates highlights

The highlights modal showed raw markdown from CHANGES.md 🚀 lines (brackets and URLs). Add a pure parse-highlight-item parser for inline links and bold, render fragments with literal hiccup in the modal (links open in a new tab), and style links and strong elements. Non-http URLs and malformed markup degrade to plain text. Adds 12 unit tests.

AI-assisted-by: muse-spark-1.3-contributor

* 🐛 Point full changelog link to main instead of staging

The view-changelog button in the check-updates modal linked to the staging branch. Point it to main, which holds the published changelog. Version detection still fetches from staging.

AI-assisted-by: muse-spark-1.3-contributor
2026-09-10 16:40:32 +02:00
Shlok Goyal
286ccb03fa
🐛 Preserve stroke dash and gap values on color change (#11557)
Signed-off-by: Shlok Goyal <shlokgoyal1279@gmail.com>
2026-09-10 14:36:51 +02:00
makesomethingshit
30849babcc
🐛 Fix fontFamilies token property mapping in Plugin API (#11566)
* 🐛 Fix fontFamilies token property mapping in Plugin API

The Plugin API exposes the font-family token property as `fontFamilies`,
while Penpot stores the canonical applied-token attribute as
`:font-family`. The bidirectional plugin/internal attribute map did not
contain that alias, so explicit `applyToken(..., ["fontFamilies"])`
validation rejected the property and applied-token readback exposed the
undocumented singular `fontFamily`.

Add `:font-family -> :font-families` to the existing canonical alias
map. The reverse mapping is derived automatically, keeping application
and readback symmetric without introducing a font-specific code path.

Closes #11405

AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>

* 🐛 Fix fontFamilies e2e test to target a text shape

The fontFamilies end-to-end regression created a flex layout frame,
whose attribute set (frame-with-layout-attributes) excludes
:font-family. The workspace token application filters such shapes,
so the internal binding and readback assertions would pass vacuously
without exercising the alias.

Target an actual `:text` shape (ctho/add-text) instead, so the test
verifies the full JS "fontFamilies" -> schema -> alias -> canonical
:font-family -> camelCase readback path.

AI-assisted-by: Omen Alpha
Signed-off-by: 최준수 <junsoo1172@gmail.com>

* 🐛 Fix fontFamilies test WASM error and add changelog entry

The text-shape fontFamilies e2e applies a layout-affecting token via
wasm renderer path, hitting missing WASM exports under Node. Merge
thw/setup-wasm-mocks! into the :each fixture and add plugins
CHANGELOG entry for the fontFamilies alias fix.

AI-assisted-by: muse-spark-1.3-contributor
Related to #11566

Signed-off-by: makesomethingshit <junsoo1172@gmail.com>

---------

Signed-off-by: 최준수 <junsoo1172@gmail.com>
Signed-off-by: makesomethingshit <junsoo1172@gmail.com>
2026-09-10 13:43:41 +02:00
Luis de Dios
c589563912
♻️ Replace digit with number in password validations (#11609) 2026-09-10 12:15:58 +02:00
andrés gonzález
0eb3179016
💄 Adjust release notes 2.18 titles (#11608) 2026-09-10 11:50:57 +02:00
Elena Torró
9b24907992
🐛 Fix render text-decoration on spans and update regression tests (#11583) 2026-09-10 09:42:09 +02:00
Juan de la Cruz
b283d952a8
✨ Add new slides content for 2.18 release (#11222)
* ✨ Add new slides content for 2.18 release

* ♻️ Use buttons from DS

* ♻️ Use new SCSS guidelines

* ♻️ Use a base stylesheet for all version files

* ♻️ Use new SCSS guidelines

* ✨ Add new images and wording

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-09-10 09:35:34 +02:00
andrés gonzález
6f63a5fcbf
✨ Persist hide resolved comments preference (#10694)
Store the hide-resolved filter in user storage and restore it when
entering the workspace or viewer, consistent with canvas comment
visibility from #10239. Match the comments filter separator styling to
the main menu and add the missing mentions option in the viewer
dropdown.

Closes #10686

Signed-off-by: Andres Gonzalez <andres.gonzalez79@gmail.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-10 08:14:37 +02:00
Andrey Antukh
66fb4a69ba 📎 Update copyright headers 2026-09-09 17:58:09 +02:00
Alejandro Alonso
ac84557740
🐛 Re-upload WASM text after WebGL context restore (#11589)
During reload-renderer!, reloading? keeps initialized?/ready? false
while set-objects runs (especially the sync path for small files).
Text content used that guard and was skipped; geometry already used
live?. Gate use-shape, has-shape, and set-shape-text-content on
wasm/live? so text is restored with the rest of the shapes.
2026-09-09 17:25:18 +02:00
Danny Shirely
d45c6710b7
🎉 Implement independent image bounds resizing (#11430)
* 🎉 Implement independent image bounds resizing

Add canvas resize interaction mode that allows users to resize an
image object's bounding box independently from the underlying bitmap
content without scaling or distortion while holding the Mod key.

AI-assisted-by: gemini-2.5-pro

* ♻️ Address reviewer feedback from elenatorro

- Remove legacy cfh/image-shape? check in shape-has-image-fill?
- Guard bounds-resize with positive dimensions instead of clamping scalev to preserve flipping
- Remove :metadata from transform-attrs in modifiers.cljs
- Restore preserveAspectRatio logic based on keep-ar? in fills.cljs
- Compute source rect against destination rect for raster and SVG fills in WASM renderer

* 🔧 Fix clippy needless borrow warnings in wasm image fills

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-09-09 15:16:56 +02:00
Alonso Torres
bf2fcba3be
🐛 Fix problems with pixel precission in WASM renderer (#11560) 2026-09-09 15:11:58 +02:00
Alexis Morin
bb6472a300
🌐 Add translations for: French (Canada)
Currently translated at 96.5% (2349 of 2432 strings)

Translation: Penpot/frontend
Translate-URL: https://hosted.weblate.org/projects/penpot/frontend/fr_CA/
2026-09-09 14:51:33 +02:00