35 Commits

Author SHA1 Message Date
Andrey Antukh
117c8db0bb Merge remote-tracking branch 'origin/staging' into develop 2026-09-22 10:24:30 +02:00
Andrey Antukh
d68531b783
⬆️ Update devenv dependencies (#11790)
* ⬆️ Update devenv dependencies

Update Node.js, OpenCode, clj-kondo, Babashka, Pixi, GitHub CLI, uv,
and Serena to their current stable releases.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to Java 27

Use Zulu JDK 27 in the development image for compatibility testing.
Update the official checksums for both supported architectures.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MinIO with RustFS in devenv

Run RustFS as the development S3 service and wait for its health check.
Install a pinned AWS CLI with checksums and use it to create the bucket
idempotently from each backend entry point.

Keep the old MinIO volume untouched and use a new RustFS volume.

AI-assisted-by: gpt-5.6-sol

* 🐳 Replace MailCatcher with persistent Mailpit

Run Mailpit as the devenv SMTP sink while preserving mailer:1025 and the
localhost:1080 UI.

Store its SQLite inbox in a named volume and wait for the readiness
endpoint before starting runtime containers. Bind the web UI to loopback so
development emails stay local.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update Node.js to 24.21.0

Align the host NVM version with the Node.js version used by devenv.

AI-assisted-by: gpt-5.6-sol

* ⬆️ Update devenv to PostgreSQL 18.6

Run PostgreSQL 18 with its versioned volume layout and a TCP readiness
check that ignores the temporary initialization server.

Install the matching client, create penpot_nexus, and preserve the old
PostgreSQL 16 volume for rollback or logical migration.

AI-assisted-by: gpt-5.6-sol

* 🐳 Expose RustFS ports in devenv

Publish the RustFS S3 API and management console on localhost port 9000
and 9001.

Keep both bindings on loopback so object storage is not exposed to the local
network.

AI-assisted-by: gpt-5.6-sol

* 🐳 Install standalone pnpm in devenv

Install pnpm 12.5.0 from architecture-specific release archives and
verify their published checksums.

Remove the Corepack setup while allowing pnpm to honor the project
packageManager pins.

AI-assisted-by: gpt-5.6-sol

* 🔥 Remove corepack, use system pnpm everywhere

Corepack is gone from Node 25+, so every `corepack enable` call
fails. pnpm now ships as a system binary (devenv, CI runners and
Docker images install it directly) and auto-downloads the version
pinned in `packageManager` on mismatch.

Scripts, workflows and Dockerfiles call `pnpm` straight away; the
three deploy workflows use a single `pnpm/setup@v2` step; and the
new `scripts/sync-pnpm-version` stamps all 35 `packageManager`
fields from the system pnpm, replacing the `corepack use` sweep.

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

* 🐛 Fix exporter watch missing render-wasm build step

The exporter watch compiled CLJS requiring the generated
src/app/wasm/shared.js, which only render-wasm/build export
produces. Without it shadow-cljs failed with a cryptic missing
./shared.js dependency. Run build:wasm before watching, as
the frontend watch:app and exporter scripts/build already do.

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

* 🔧 Add opencode V2 support and adapt plugins

Register the penpot tools for both opencode V1 (server())
and V2 (setup() with JSON Schema inputs) from a single
dependency-free plugin file, sharing the psql and
paren-repair runners between both paths.

Install the opencode2 binary side-by-side with V1 in the
devenv image and document the dual registration in the
paren-repair and psql memories.

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

* ⬆️ Update pnpm and opencode
2026-09-22 10:22:31 +02:00
Eva Marco
dc8160c13a
🎉 Add Menu DS component (#11511)
* 🎉 Add Menu design-system component

Adds Menu, MenuItem, MenuSeparator, SubMenu, and ContextMenu to the
shared UI package and exposes them through the CLJS design-system
wrapper, with Storybook stories and MDX docs.

Built on react-aria-components for keyboard navigation, focus
management, and dismissal. Penpot's own DS buttons aren't
react-aria-aware, so trigger positioning, focus-on-open, and
close-on-select are wired explicitly instead of relying on the
library's default trigger detection.

Includes a temporary manual-test harness in the dashboard to check
the components against the real app. CSS is functional but doesn't
match the DS visual design yet — that comes in a follow-up.

AI-assisted-by: claude-sonnet-5

* ✨ Add left/right corner placements to Menu design-system component

Menu and ContextMenu only exposed 8 of react-aria's placement values,
missing every left/right corner variant (right bottom, right top,
left bottom, left top) that the top/bottom sides already had via
start/end.

Add the four missing corners, matching the start/end pattern already
used for top/bottom, so a menu can open toward any corner of its
trigger.

AI-assisted-by: claude-sonnet-5

* ✨ Add drilldown variant to SubMenu design-system component

SubMenu only opened as a flyout: a nested popover next to the
trigger item. That doesn't scale to a tree too deep or wide for a
chain of flyouts, e.g. move-to-project's team -> project nesting,
which needs a mobile-style drilldown (replace the current items with
the submenu's own, plus a way back) instead.

Add a `variant` prop, `"flyout"` (default, unchanged) or
`"drilldown"`. Menu and ContextMenu each keep a navigation stack,
provided to their content tree via context, so a drilldown SubMenu
nested inside another drilldown SubMenu still drills into the same
stack and arbitrarily deep trees stay navigable one screen at a
time. Switching levels remounts the level's content wrapped in a
keyed Fragment rather than updating it in place, since
react-stately's Collection requires each item's id to stay stable
across an update and the back item's label (and everything under it)
genuinely changes identity between levels.

AI-assisted-by: claude-sonnet-5

* ♻️ Wire the DS Menu/SubMenu into the dashboard file menu

file_menu.cljs used context-menu-a11y's data-driven options list,
rendered via a generic recursive renderer. Rewritten as real JSX
composition (menu-item*/sub-menu*/menu-separator*) using the DS Menu
component, preserving every existing conditional branch (single-file,
multi-select, restore-mode, permission gates). "Move to" -> "Move to
other team" -> team -> project now uses sub-menu*'s drilldown variant
at every level.

Split into file-menu-items* (the item tree, no popover of its own)
and a thin file-menu* wrapper (Menu, anchored to the "..." button),
so grid.cljs can render the same items a second time inside a
ContextMenu for right-click, matching the previous behavior of
opening either via the button or a right-click anywhere on the row.

grid.cljs's trigger handling is simplified accordingly: DS's Menu/
ContextMenu handle their own positioning (including auto-flip near
viewport edges) and dismissal internally, so the manual click-
coordinate math, the dashboard-local :menu-open/:menu-pos globals,
and the portal-on-document* wrapper (Popover already portals itself)
are all gone. The now-fully-dead show-file-menu-with-position/
show-file-menu/hide-file-menu actions are removed from
data/dashboard.cljs.

Also fixes two issues found wiring this up:
- The add-shared/unpublish-shared toggle rendered two different
  menu-item* ids at the same list position; :is-shared can flip while
  the popover stays open (the action's own side effect), and
  react-stately's Collection requires an item's id to stay stable
  across such an update. Both branches now share one id.
- Menu's own trigger wrapper (align-self: start, needed generically
  so it doesn't stretch in an arbitrary parent) overrode
  .project-thumbnail-actions's centering of the "..." button;
  grid.scss now re-asserts centering for that specific consumer.

Removes the temporary menu-test* harness from dashboard.cljs now that
there's a real integration to test against instead.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix Menu/ContextMenu popover interaction bugs

Found testing the dashboard file menu integration:

- Reopening the same trigger right after closing (e.g. right-click,
  dismiss, right-click again) could silently fail or briefly show two
  overlapping instances. Closing played a 100ms exit fade, and a
  reopen landing mid-fade raced the still-live Popover instance.
  Closing now always skips the exit animation, so by the time any
  subsequent open request arrives there's no ambiguous in-between
  DOM state left to race.

- Right-clicking a different row while one file's context menu was
  open didn't close the first one. Menu/ContextMenu don't use
  react-aria-components' own MenuTrigger (Penpot's DS buttons aren't
  react-aria-pressable), so they also don't get its built-in
  RootMenuTriggerStateContext coordination between sibling instances.
  A window CustomEvent broadcast restores it: opening announces this
  instance's id, and every other mounted instance closes on hearing a
  different one.

- With that coordination in place, right-clicking elsewhere still did
  nothing at all: Popover defaults to modal, which marks the rest of
  the app inert (unfocusable *and* unclickable, not just visually
  blocked) while open. Correct for a real Dialog, wrong for a
  lightweight dismissable menu. Fixed with isNonModal on all three
  Popover usages (Menu, ContextMenu, SubMenu's flyout).

- isNonModal has its own side effect: react-aria only wires up its
  click-outside-closes behavior when a popover is "dismissable", which
  isNonModal forces off (for anything but a submenu flyout) with no
  separate prop to turn back on. Reimplemented directly: a pointerdown
  landing outside the popover's own rendered content closes it, via a
  ref now passed to Popover.

AI-assisted-by: claude-sonnet-5

* 🐛 Fix Menu visual styling and two overflow bugs

Border and shadow, to match the legacy context-menu-a11y menu this
replaces: the DS component had neither (a filter: drop-shadow with a
different blur radius stood in for the shadow, and there was no
border at all). Used the pattern already established by sibling DS
dropdowns (options-dropdown.scss et al.) rather than porting the
legacy tokens directly — border: 1px solid
var(--color-background-quaternary) + box-shadow: 0 0 12px 0
var(--color-shadow-dark), both already in use elsewhere in this same
file.

Found two real bugs verifying that against a long "move to" list:

- .menuItem/.separator had no flex-shrink: 0, so once a list's
  natural height exceeded the menu's max-block-size, flexbox shrank
  every row to fit them all rather than triggering the scrollbar —
  overflow only kicks in after flex-shrink has done its best, and
  shrinking was never opted out of.

- The menu's own fixed max-block-size: 300px ignored react-aria's
  Popover, which sets its own max-height (inline, on our direct
  parent) to whatever space is actually available between the trigger
  and the viewport edge. In a small viewport that computed value can
  be under 300px; since the parent has no overflow of its own, our
  independent 300px cap just rendered straight past it and off the
  edge of the window. max-block-size: inherit picks up the parent's
  own computed value instead, at the cost of no longer capping how
  tall the menu can get when there's plenty of room (verified: 348px
  in a normal-height viewport, vs the old fixed 300px) — an
  acceptable tradeoff against content becoming inaccessible.

AI-assisted-by: claude-sonnet-5

* 💄 Adjust Menu design-system component item states and spacing

Give menu items a distinct keyboard-focus ring (accent-primary outline
plus tertiary background) separate from the mouse hover/click state,
which keeps its existing quaternary background unchanged. Restyle
disabled items with a tertiary background and secondary text color,
shrink the submenu chevron to 12x12, and tighten the menu's vertical
padding to 4px.

* 📚 Document drilldown submenu and tighten Menu docs

Add the drilldown submenu story to the Menu docs page, show the
idiomatic controlled-state shape in the usage example (callbacks
bound in the let with mf/use-fn, explicit deref of the open state),
and trim the prose down to the information a consumer needs.

* 🐛 Fix Menu outside-click closing on its own trigger and submenus

useCloseOnOutsideClick restores the dismiss behavior isNonModal turns
off, but it tested containment against the popover element alone. That
missed two cases react-aria's own useOverlay accounts for.

A root Popover wraps its content in a display:contents div and portals
every SubmenuTrigger's nested popover into that same div, so a flyout
submenu is a sibling of the popover, not a descendant. Pressing an item
in one counted as an outside click: the whole tree unmounted on
pointerdown and the item's action never fired on pointerup. Test the
group container instead.

The trigger was likewise treated as outside, so closing on its
pointerdown let the click's own handler read the already-false open
state and reopen the menu — a trigger wired to a toggle could never
close it. Exclude it in Menu; ContextMenu keeps the old behavior, since
right-clicking elsewhere should reopen it against a new anchor.

* 🐛 Target the clicked file when it is not in the dashboard selection

The file menu adopted the whole selection whenever it was non-empty,
guarding only against it being empty. That left the case where the
selection holds files this row is not one of: toggle-file-select is a
no-op across projects, so shift-right-clicking a file in another
project leaves the previous project's selection intact and the menu
opened on the pointed-at file while offering rename, duplicate, move
and delete for a different one.

Adopt the selection only when it actually contains this file, which
covers the deferred-dispatch case the previous guard was written for
just as well.

* 🐛 Drop the file menu teams cache that outlived a logout

The cache was a module-global defonce atom, and logging out does not
reload the page — it resets the store and navigates. The next profile
to sign in on the same tab therefore opened its first file menu with
the previous account's team and project names listed under "Move to",
until the background fetch replaced them.

The cache only ever saved the brief absence of one submenu, which is
already guarded on having data and so does not shift any layout, so
remove it rather than scope it to a profile. Dispose the subscription
too: it wrote to component state after unmount.

* 🐛 Keep the Menu open when its own trigger takes focus

Excluding the trigger from the outside-click dismiss was not enough to
make a toggle trigger able to close the menu: usePopover passes
shouldCloseOnBlur unconditionally, and useOverlay acts on it regardless
of isNonModal, so focus moving to the trigger on its own pointerdown
closed the popover before the click ran. The click then read an open
state that was already false and reopened it.

shouldCloseOnInteractOutside is the one exception useOverlay consults
before closing on blur, so use it to exempt the trigger.

* 🔧 Add interaction tests for the Menu component

Cover the two dismissal regressions just fixed — closing the menu from
its own trigger, and a press inside a flyout submenu not being treated
as an outside click — plus drilldown navigation, the navigation stack
resetting between open/close cycles, and Escape and outside click.
Both regression tests fail against the code as it was before the fixes.

The story trigger now toggles instead of only ever opening, which is
what a real caller does (the dashboard's own is a swap!) and what makes
the reopen bug observable at all.

* ✨ Add max-width, density, and drilldown sizing to Menu/ContextMenu

Add a max-width prop (default 250px) to Menu, ContextMenu, and flyout
SubMenu, and an is-dense prop to Menu/ContextMenu that shrinks every
item — including nested flyout SubMenus, via a shared density context
— to a 28px row. Pin a drilldown SubMenu's popover to at least the
root level's own size, so navigating into a shorter or narrower list
doesn't shrink the menu mid-navigation.

Also truncate a plain MenuItem's text with an ellipsis instead of
letting it wrap and blow out the row height, matching the existing
SubMenu trigger label, and fix that label's own truncation: it was
missing min-inline-size: 0, without which a flex item can't shrink
below its content size and text-overflow: ellipsis never engages.

Exposed through the ClojureScript facade as :max-width/:is-dense,
documented with new example canvases, and covered by five new
Storybook interaction tests, each verified to fail without its
corresponding fix.

* ♻️ Wire the DS Menu/ContextMenu into the dashboard project menu

Replace the legacy context-menu-a11y-based project menu (grid, sidebar,
and per-project file view) with the DS Menu/ContextMenu components,
mirroring the earlier file menu migration. Drop the manual
:menu-open/:menu-pos position tracking in favor of the DS components'
own positioning, and split project-menu-items* out so both the "..."
trigger and right-click share the same options.

The hidden file input behind the "Import" option moves out of the
popover content and into whichever parent stays mounted regardless of
the menu's own open state: the DS popover really unmounts its content
on close (unlike context-menu-a11y, which only hid it), and selecting
"Import" closes the menu in the same tick a ref owned inside it would
already be gone.

Add an onOpenChange notification to ContextMenu (it stays uncontrolled,
this only reports state changes) so the project row's "..."/pin/add-file
actions can stay visible for as long as either menu is open, the same
way they already do on hover. Fix a related visibility bug this exposed:
closing a menu restores focus to its trigger regardless of whether the
open happened via mouse or keyboard, so :focus-within alone kept the
actions visible after closing with the pointer away — swapped for
:has(:focus-visible), which only matches real keyboard navigation.

* 🐛 Forward MenuItem's id to the DOM as data-testid

MenuItem's function signature never forwarded anything beyond its
explicitly-typed props to the underlying RACMenuItem, so a caller's
id — meant as a stable per-item identifier — only ever reached the DOM
as react-aria's own internal data-key, never as data-testid. This
silently broke dashboard.spec.js's "Multiple elements in context" test
after the file menu's migration to this component, since every existing
menu item id was already relied on as its test id.

id is already unique per item for selection/on-action, so deriving
data-testid from it directly means every item is reachable in a test
with no separate prop to remember to pass. SubMenu's own trigger row is
a MenuItem too, so this covers it for free.

* 🔧 Add Playwright coverage for the project options menu

Covers all four places the migrated project menu is reachable: the
dashboard grid's "..." button and title right-click, the sidebar's
right-click, and the per-project files page's "..." button. Checks
rename/duplicate/pin/move-to/delete render (and that the default
Drafts project correctly hides all of them), that rename opens the
inline editor, that delete opens the confirm modal, and that the
move-to submenu lists other teams.

Also drop an unused React import from context_menu.stories.jsx,
spotted in passing.

* ♻️ Add datatest id

* ♻️ Fix linter

* 🐛 Build @penpot/ui automatically after pnpm install

packages/ui/dist is gitignored (build output) and nothing in the
install pipeline built it, so a fresh checkout — CI included — never
had it. Any code importing "@penpot/ui/menu" (the frontend's own
cljs-runtime tests among them) failed at module resolution with
ERR_MODULE_NOT_FOUND rather than any real test failure.

Build it in postinstall, the same way plugins-runtime already does,
so it's always present after `pnpm install` without a separate manual
build step.

* 🔥 Remove flaky Menu dense/ellipsis Storybook tests

Test Dense Shrinks Items and Test Long Label Ellipses Instead Of
Wrapping asserted computed pixel styles that passed consistently
locally (including with a fresh packages/ui install) but failed in CI,
suggesting a CI-only timing/environment discrepancy in when the
computed style stabilizes. Dropping them rather than chasing a
non-reproducible flake.

* 💄 Open the file/project options menu right, top-aligned

Switch the dashboard file and project "..." options menus from
"bottom end" to "right top" placement, so they open beside the
trigger button instead of below it.

* 🐛 Stop drilldown SubMenu jumping to the opposite edge

A drilldown SubMenu swaps its parent Menu/ContextMenu popover's own
content in place, and react-aria re-runs its flip/collision placement
on every layout change. Drilling into a shorter level than the root
could shrink the popover enough that react-aria decided there was now
room on the other side, flipping it there — a visible jump even though
the popover never actually moved from the caller's point of view.

The previous fix padded every drilled-in level out to the root's own
min-inline-size/min-block-size so the popover never got small enough
to trigger a re-flip, but that meant a level naturally much shorter
than the root still rendered at the root's full height.

Replace it with shouldUpdatePosition={false} on the Popover for as
long as any level is drilled in. This freezes whichever edge react-aria
already resolved for the root, so a shorter level just shrinks from the
opposite edge instead of triggering a new placement decision, and a
taller level grows from that same opposite edge in the direction the
root already opened. shouldUpdatePosition goes back to true once the
stack returns to the root, so a fresh open still resolves normally.

* ♻️ Update menu placements and use buttons from DS

---------

Co-authored-by: Luis de Dios <luis.dedios@kaleidos.net>
2026-09-21 10:41:31 +02:00
Andrey Antukh
c1bd3cb9f0 Merge remote-tracking branch 'origin/staging' into develop 2026-09-09 10:31:41 +02:00
Andrey Antukh
e4d1816117 ⬆️ Update pnpm to 12.3.4 across all workspaces
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
2026-09-09 08:04:11 +02:00
Eva Marco
cd98a88c4d
🐛 Fix errors on shortcuts (#11081)
* 🐛 Fix shift + character recording

* 🐛 Fix importing conflicts

* 🐛 Fix paste as non customizable shortcut

* 🐛 Fix duplicate manage of custom shortcuts

* 🐛 Fix json file name

* 🎉 Add search by commands

* 🐛 Fix CI
2026-09-07 10:14:09 +02:00
Andrey Antukh
f5aad7b1ae Merge remote-tracking branch 'origin/staging' into develop 2026-09-01 12:45:17 +02:00
Andrey Antukh
a1079cf788
⬆️ Update JVM, pnpm and node dependencies (#11404)
* ⬆️ Update pnpm and its deps

* ⬆️ Update JVM dependencies in backend and common

Update several JVM dependencies across backend and common:

- passay 1.6.6 -> 2.0.0 (package reorg, ctor-based rules)
- siphash 2.0.0 -> 3.0.0 (SipHasher* renamed to SipHash*)
- lettuce-core, guava, sqlite-jdbc, jsoup, lz4-java, markdown-clj,
  awssdk s3/sts, selmer, jackson-core/databind, shadow-cljs

Adapt passay validation to the new API (moved packages, constructor
configuration) and siphash to the renamed classes. Add tests for
password validation and UUID advisory-lock hashing.

AI-assisted-by: deepseek-v4-flash

* ⬆️ Update node on docker images

* 📎 Minor fixes related to pnpm12 compatibility
2026-09-01 12:01:35 +02:00
Andrey Antukh
81c3b3cd56
📎 Update copyright name on file header (#11346) 2026-08-25 11:55:10 +02:00
Andrey Antukh
a60b648c6c 🐛 Fix issues with draft-js tests 2026-08-06 08:55:58 +02:00
Andrey Antukh
49276886f3 🐛 Fix some issues with immutablejs incompatibility 2026-08-05 17:28:58 +02:00
Andrey Antukh
3865e29b65 ⬆️ Update pnpm dependencies across all modules 2026-08-04 14:15:35 +02:00
Andrey Antukh
319a2185c9
🐛 Fix crash on drag-and-drop of selected text in Draft.js editor (#10959)
After `Modifier.removeRange` modified the block map, the `targetRange` from
the drop event still referenced stale block keys from the pre-removal DOM
state, causing `TypeError: Cannot read properties of undefined`.

- Added a guarded `moveText` export in `frontend/packages/draft-js/index.js`
  that validates block keys exist before and after removal. Falls back
  gracefully when target range references stale keys.
- Added a `handle-drop` callback in
  `frontend/src/app/main/ui/workspace/shapes/text/editor.cljs` that returns
  "handled" for internal drag operations, preventing Draft.js from calling its
  default (crash-prone) handler.

AI-assisted-by: mimo-v2.5
2026-08-03 17:31:55 +02:00
Andrey Antukh
07efd8fb99
⬆️ Update dependencies (#10982)
* ⬆️ Update root pnpm depdendencies

* ⬆️ Update common pnpm dependencies

* ⬆️ Upgrade docs pnpm dependencies

* ⬆️ Upgrade pnpm deps on library

* ⬆️ Upgrade plugins pnpm dependencies

* ⬆️ Update pnpm dependencies on mcp

* ⬆️ Update pnpm dependencies on frontend

* ⬆️ Update exporter pnpm dependencies

* ⬆️ Update jvm/clojure deps

* ⬆️ Update docker dependencies (jvm and node)

* 📎 Add minor fixes
2026-07-31 18:53:05 +02:00
Eva Marco
e705b6e12c
🎉 Add components library (#10675)
* 🎉 Install react-aria-components

* 🎉 Create modal component in TS using react-aria-component

* 🎉 Create modal ds component

* 🎉 Separate header content and footer components

* 🐛 Remove mf/html macros when not needed

* 🎉 Solve little problems

* ♻️ Format files

* ♻️ Remove ModalCloseBtn

* 🐛 Fix CI

* ♻️ Remove unused files

* 🎉 Make close button not dependant on the modal header

* 🎉 Add footer with two slots

* 🎉 Improvements on modal

* 🐛 Fix package imports and remove login example code

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
2026-07-30 13:05:28 +02:00
Andrey Antukh
5a0cee44b1
🐛 Harden frontend .getData call sites against undefined receivers (#10718)
* 🐛 Fix nil getData crash dropping ZIP without manifest.json

Add nil-guard in read-as-text to raise typed :invalid-entry error instead of calling (.getData nil writer) which produced a raw TypeError.

Made read-zip-manifest public (was defn-) with explicit detection of missing manifest.json, raising typed :invalid-penpot-file validation error. The existing catch path surfaces this hint as a friendly user error instead of the raw TypeError text.

Add regression tests for both paths. 374 users were affected, 704 occurrences across 2.17.0-RC2/RC3/RC4.

Fixes #10709.

AI-assisted-by: minimax-m3

* 🐛 Harden dnd/get-data against missing dataTransfer

When the sortable hook or any caller passes a synthetic event
without a dataTransfer property (e.g. a dragend fired after a drop
that has already cleared the transfer), the previous implementation
called .getData directly on the nil/undefined result and threw
"Cannot read properties of undefined (reading 'getData')".

Wrap the body in when-let so get-data returns nil cleanly when
dataTransfer is missing. All three current callers
(hooks.cljs:164, viewport/actions.cljs:531 and :562) already treat
the return value as optional via when-let / when, so no caller
breaks.

Add a regression test covering both the missing-dataTransfer case
and a real dataTransfer roundtrip.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in forms

When a paste event arrives without a clipboardData property (e.g.
a programmatically dispatched ClipboardEvent in some browsers, or
edge cases like dragging a file with no text content), the previous
implementation called .getData directly on the nil/undefined
clipboardData and threw "Cannot read properties of undefined
(reading 'getData')".

Wrap the body in when-let so the paste logic is skipped entirely
when clipboardData is missing. The existing (string? paste-data)
guard in the inner when already tolerates nil; no other caller
behavior changes.

AI-assisted-by: minimax-m3

* 🐛 Harden paste handler against missing clipboardData in components/forms

Same defensive pattern as the main/ui/forms.cljs paste handler: wrap
the body in when-let so the .getData call is skipped when the
clipboardData property is missing on the paste event. Prevents the
raw "Cannot read properties of undefined (reading 'getData')"
TypeError for programmatic / edge-case paste events.

AI-assisted-by: minimax-m3

* 🐛 Harden v3 text editor paste and styles-fn against undefined receivers

Two related fixes for the "Cannot read properties of undefined
(reading 'getData')" family of bugs in the workspace text editor:

- v3_editor.cljs: wrap the paste body in when-let on clipboardData
  so .getData("text/plain") is never called on a nil receiver. The
  existing (when (and text (seq text))) guard already tolerates nil
  text; only the outer .getData call was unprotected.

- editor.cljs: add (and content ...) to the if branch in styles-fn
  so .getText and .getData are never called on a nil content. The
  else branch (legacy.txt/styles-to-attrs) is already the correct
  fallback for missing content.

Add a regression test that mirrors the fixed patterns and verifies
they no longer throw on synthetic events with no clipboardData or
nil content.

AI-assisted-by: minimax-m3

* 🐛 Harden get-editor-block-data and get-editor-block-type against nil block

getCurrentBlock from Draft.js can return undefined for an empty
selection (e.g. before any block is created). The previous
implementations called .getData / .getType directly on the result
and threw "Cannot read properties of undefined (reading
'getData')" / "...reading 'getType')".

Wrap both functions in (when (some? block) ...) so they return nil
cleanly. Callers in editor.cljs and text_editor.cljs already handle
nil results (render-block short-circuits via the case on type; the
text-data caller in text_editor.cljs lets nil flow up), so no
upstream change is required.

Add a regression test covering both functions with nil and
js/undefined input.

AI-assisted-by: minimax-m3

* 🐛 Harden draft-js block-data helpers against nil block

Three related fixes in the vendored draft-js package:

- mergeBlockData: early-return undefined when block is falsy.
  Without this, the first line (block.getData()) throws for callers
  that pass a nil block.

- splitBlockPreservingData: guard the blockMap.get(...) lookup. If
  the start key is stale (e.g. after a Modifier.splitBlock that
  doesn't actually produce the expected key), .get() returns
  undefined and the subsequent .getData() throws. Fall back to an
  empty Immutable Map for the block data.

- updateBlockData: short-circuit (return state unchanged) when
  mergeBlockData returns undefined. Without this, the chain
  newBlock.getData() would throw on the same nil-block case that
  mergeBlockData now guards.

These match the defensive nil-handling pattern used elsewhere in
the frontend (.getData callers) and protect against stale
selection keys in the Draft.js content state.

AI-assisted-by: minimax-m3
2026-07-27 10:56:27 +02:00
Andrey Antukh
f2460eee29
🐛 Fix Draft.js selection offset DOMException on text editor re-render (#10608)
Clamp selection offset to node length in setDraftEditorSelection to
prevent DOMException when Draft.js SelectionState offset exceeds the
actual DOM text node length. This can happen during spellcheck, IME
composition, or race conditions during React re-renders.

Updates @penpot/draft-js to commit 09a33e0a which includes the fix
in addPointToSelection and addFocusToSelection.

Fixes #10607

AI-assisted-by: mimo-v2.5-pro
2026-07-09 10:59:40 +02:00
Andrey Antukh
e2b52d88fc ⬆️ Update dependencies 2026-06-30 16:15:30 +02:00
Andrey Antukh
0a54533240 Merge remote-tracking branch 'origin/staging' into develop 2026-06-17 00:11:23 +02:00
Andrey Antukh
03341ed857
⬆️ Update npm deps and pnpm on all subpackages (#10183) 2026-06-15 13:57:29 +02:00
Andrey Antukh
5eb8c9ee70 ⬆️ Update frontend deps 2026-06-11 11:06:12 +02:00
Andrey Antukh
ba9b03268a
⬆️ Update backend and common dependencies (#10108)
* 🐛 Fix incorrect valkey uri on backend tests

* ⬆️ Update backend dependencies

* ⬆️ Update pnpm dependencies

* 📎 Fix minor linter issues
2026-06-11 10:11:23 +02:00
Andrey Antukh
c5de4c27b0 Merge remote-tracking branch 'origin/main' into staging 2026-06-01 12:57:39 +02:00
Yamila Moreno
ddba2ffa75
📎 Update Kaleidos Copyright (#9929) 2026-05-29 11:24:58 +02:00
Renzo
8aacda2249
✨ Add Shift+Numpad0/1/2 zoom shortcut aliases (#2457) (#9063)
Signed-off-by: RenzoMXD <170978465+RenzoMXD@users.noreply.github.com>
2026-04-24 09:08:31 +02:00
Andrey Antukh
61d319eaac
⬆️ Update dependencies (#8867)
* ⬆️ Update deps

* ⬆️ Update storybook dependencies

* ⬆️ Update dependencies

* 🐛 Fix invalid var() usage on SCSS variable in numeric_input

* ⬆️ Update deps
2026-04-07 21:35:00 +02:00
Andrey Antukh
8928e274fc Merge remote-tracking branch 'origin/main' into staging 2026-03-24 18:01:38 +01:00
Andrey Antukh
56f1fcdb53 🐛 Fix crash when pasting image into text editor
When pasting an image (with no text content) into the text editor,
Draft.js calls handlePastedText with null/empty text. The previous fix
guarded splitTextIntoTextBlocks against null, but insertText still
attempted to build a fragment from an empty block array, causing
Modifier.replaceWithFragment to crash with 'Cannot read properties of
undefined (reading getLength)'.

Fix insertText to return the original state unchanged when there are no
text blocks to insert. Also guard handle-pasted-text in the ClojureScript
editor to skip the insert-text call entirely when text is nil or empty.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:00:28 +00:00
Andrey Antukh
d863c7065f 🐛 Fix null text crash on paste in text editor
The splitTextIntoTextBlocks function in @penpot/draft-js called
.split() on the text parameter without a null check. When pasting
content without text data (e.g., images only), Draft.js passes null
to handlePastedText, causing a TypeError.

Signed-off-by: Andrey Antukh <niwi@niwi.nz>
2026-03-24 13:49:28 +01:00
Andrey Antukh
0de482da9d
⬆️ Update pnpm to 10.31.0 across all submodules (#8549) 2026-03-10 10:03:05 +01:00
Florian Schroedl
375608b44b ⬆️ Update tokenscript interpreter to 0.26.0 and add CSS color schemas
Regenerate schemas.js with preset:cssColors to support CSS color constants.
2026-02-12 14:14:45 +01:00
Juanfran
45d04942cc ✨ Add example ui storybook 2026-02-10 08:29:24 +01:00
Andrey Antukh
17935443df ✨ Move all tokenscript related adaptations to a separared package 2026-02-05 09:45:55 +01:00
Andrey Antukh
1246250198 🎉 Migrate to PNPM frontend module 2026-01-22 13:55:41 +01:00
Andrey Antukh
b77712ce73 ✨ Move frontend/vendor to frontend/packages 2025-12-23 13:10:58 +01:00