diff --git a/.serena/memories/critical-info.md b/.serena/memories/critical-info.md
index 9033a9e74a..0854fe28ee 100644
--- a/.serena/memories/critical-info.md
+++ b/.serena/memories/critical-info.md
@@ -14,6 +14,9 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- Before `git commit` → `mem:workflow/creating-commits` (subject format, body, `AI-assisted-by: model-name` trailer)
- Before `gh issue create` → `mem:workflow/creating-issues` (title derivation, body template, labels, Issue Type)
- Before `gh pr create` / `gh pr edit` → `mem:workflow/creating-prs` (title format, body structure, "Note:" line)
+- Before a repo-wide pnpm version update → `mem:workflow/updating-pnpm` (workspace
+ layout, `corepack use` sweep order, the stamp-missing-field and
+ ignored-builds gotchas, verification steps)
- **Never `git push`, force-push, or modify `git origin`** (or any other remote). The user pushes from their own shell; if a push is required, say so and wait. Never amend a commit that the user has already pushed unless explicitly asked.
- **Never edit `CHANGES.md` by hand.** The changelog is generated from GitHub milestones during the release process; update it only via the `update-changelog` skill flow or on explicit user request.
- You have access to the GitHub CLI `gh` or corresponding MCP tools.
@@ -71,6 +74,10 @@ module. You can read it from `mem:/core`
- `scripts/error-reports.mjs` — Query error reports via RPC API with token
authentication. Supports list/get operations with filtering and pagination.
See `mem:scripts/error-reports`.
+- `scripts/clean-node-modules` — Remove stale `node_modules` from all pnpm
+ workspaces (root, modules, member packages). Keeps the shared pnpm store
+ at `/.pnpm-store` unless `--store`; ignores `external/` and
+ `.opencode/`. Usage and reinstall steps: `mem:workflow/updating-pnpm`.
# Dependency graph
diff --git a/.serena/memories/workflow/updating-pnpm.md b/.serena/memories/workflow/updating-pnpm.md
new file mode 100644
index 0000000000..9f4ba133df
--- /dev/null
+++ b/.serena/memories/workflow/updating-pnpm.md
@@ -0,0 +1,89 @@
+# Updating pnpm Across All Workspaces
+
+Canonical procedure. Run it from the repo root with the log redirected to a
+file (never pipe tool output through filters).
+
+## Layout facts
+
+- The repo has 11 pnpm workspaces, each with its own `pnpm-workspace.yaml`
+ and `pnpm-lock.yaml`: the repo root plus `backend`, `common`, `docs`,
+ `exporter`, `frontend`, `library`, `mcp`, `media-processor`, `plugins`,
+ and `render-wasm`.
+- Every package inside a module workspace (for example all `plugins/apps/*`
+ and `plugins/libs/*` packages) is a plain member of that module's
+ workspace. Members must not carry their own `pnpm-workspace.yaml` or
+ `pnpm-lock.yaml`; their dependencies resolve through the parent
+ workspace's lockfile.
+- One shared pnpm store for the whole repo: `/.pnpm-store`. Every
+ workspace yaml sets it explicitly: `storeDir: .pnpm-store` at the root,
+ `storeDir: ../.pnpm-store` in each module. pnpm resolves the value
+ against the workspace root, so all workspaces land on the same store.
+ Do not remove these lines: nested workspaces do not inherit settings,
+ and without them each workspace may resolve a different store.
+- The store survives `node_modules` cleans. It is content-addressed and
+ integrity-verified, so it cannot go stale; staleness lives in
+ node_modules. Only `scripts/clean-node-modules --store` removes it.
+- Every `package.json` (about 35 of them) must carry a `packageManager` field
+ with the identical `pnpm@+sha512.` value. Do not let them drift.
+- CI pins no pnpm version; workflows rely on corepack reading
+ `packageManager`. Fixing the fields fixes CI.
+
+## Procedure
+
+1. Resolve the target tag first and note the version. Example:
+ `npm view pnpm dist-tags --json` for `next-12` (latest 12.x). The tag
+ moves over time; always re-check.
+2. List every directory with a `package.json`, excluding `node_modules`
+ (`fd -H -t f package.json -E node_modules`). This list is the work set;
+ do not maintain a hand-written list.
+3. Run `corepack use pnpm@` in workspace roots first, then members.
+ `corepack use` stamps `packageManager` in the nearest package.json and
+ runs an install. Member runs repeat the workspace install; after the root
+ run they are quick no-ops.
+4. If a run fails, fix the cause (see gotchas) and re-run that directory.
+
+## Gotchas
+
+- `corepack use` only updates an existing `packageManager` field. If a
+ package.json lacks the field, corepack walks up to the nearest ancestor
+ that has one and stamps that file instead; the member stays unstamped.
+ After the sweep, assert every package.json carries the field. For a
+ missing one, insert the identical `pnpm@+sha512.` string,
+ then re-run `corepack use pnpm@` in that directory.
+- A workspace may fail with `ERR_PNPM_IGNORED_BUILDS`, and pnpm then writes
+ a placeholder scaffold into its `pnpm-workspace.yaml`:
+ `allowBuilds: esbuild: set this to true or false` plus
+ `ignoredBuiltDependencies`. Repo convention is `allowBuilds: esbuild: true`.
+ Replace the placeholder and drop the `ignoredBuiltDependencies` entry,
+ then re-run.
+- `plugins/apps/composable-test-suite` once had its own
+ `pnpm-workspace.yaml` and acted as a nested workspace root. That state is
+ gone on purpose: pnpm picks the nearest `pnpm-workspace.yaml` walking up,
+ so a nested one silently forks install and lockfile behavior. Do not
+ reintroduce it.
+- Expect metadata-only lockfile diffs when only the pnpm version moves:
+ the pnpm self-reference entries, plus a new `packageManagerDependencies`
+ section in lockfiles last written by older pnpm. Large diffs mean
+ re-resolution; inspect them before accepting.
+
+## Verification
+
+- Every `packageManager` field is byte-identical (same version and hash).
+- `pnpm --version` in each workspace prints the target version.
+- `pnpm install --frozen-lockfile` succeeds in each of the 11 workspaces.
+- `git diff` on lockfiles matches the expectations above.
+
+## Cleaning stale node_modules
+
+- `scripts/clean-node-modules` removes every workspace `node_modules`: the
+ repo root, all module workspaces, and all member packages. Use it when
+ installs misbehave after dependency changes: clean, reinstall, done.
+- Flags: `-n/--dry-run` lists without deleting; `--store` also removes the
+ shared pnpm store at `/.pnpm-store` (the next install re-downloads
+ what it held). `external/` (vendored dependency trees with their own
+ lifecycles) and `.opencode/` are always ignored.
+- The script never touches the pnpm store by default, so the reinstall
+ after cleaning reuses cached packages (zero downloads).
+- After cleaning, run `pnpm install` in each workspace root to restore the
+ development environment; `frontend` postinstall also reinstalls and
+ builds `plugins-runtime`.
diff --git a/backend/package.json b/backend/package.json
index c6baf43f73..f29469a28e 100644
--- a/backend/package.json
+++ b/backend/package.json
@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
- "packageManager": "pnpm@11.20.0+sha512.9a6f330a95b66446ea088faf1521405a8a01f07fde7124cc9958dfed52d4bb436737e65b08f85f37b46fcba375092558ac51262b816844b22f63406ed166bfee",
+ "packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
diff --git a/backend/pnpm-lock.yaml b/backend/pnpm-lock.yaml
index a0b16465e9..dfe9e50563 100644
--- a/backend/pnpm-lock.yaml
+++ b/backend/pnpm-lock.yaml
@@ -1,3 +1,104 @@
+---
+lockfileVersion: '9.0'
+
+importers:
+
+ .:
+ configDependencies: {}
+ packageManagerDependencies:
+ pnpm:
+ specifier: 12.3.4
+ version: 12.3.4
+
+packages:
+
+ '@pnpm/exe.darwin-arm64@12.3.4':
+ resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
+ cpu: [arm64]
+ os: [darwin]
+
+ '@pnpm/exe.darwin-x64@12.3.4':
+ resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
+ cpu: [x64]
+ os: [darwin]
+
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
+ resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-arm64@12.3.4':
+ resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
+ cpu: [arm64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.linux-x64-musl@12.3.4':
+ resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
+ cpu: [x64]
+ os: [linux]
+ libc: [musl]
+
+ '@pnpm/exe.linux-x64@12.3.4':
+ resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
+ cpu: [x64]
+ os: [linux]
+ libc: [glibc]
+
+ '@pnpm/exe.win32-arm64@12.3.4':
+ resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
+ cpu: [arm64]
+ os: [win32]
+
+ '@pnpm/exe.win32-x64@12.3.4':
+ resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
+ cpu: [x64]
+ os: [win32]
+
+ pnpm@12.3.4:
+ resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
+ engines: {node: '>=18.*'}
+ hasBin: true
+
+snapshots:
+
+ '@pnpm/exe.darwin-arm64@12.3.4':
+ optional: true
+
+ '@pnpm/exe.darwin-x64@12.3.4':
+ optional: true
+
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
+ optional: true
+
+ '@pnpm/exe.linux-arm64@12.3.4':
+ optional: true
+
+ '@pnpm/exe.linux-x64-musl@12.3.4':
+ optional: true
+
+ '@pnpm/exe.linux-x64@12.3.4':
+ optional: true
+
+ '@pnpm/exe.win32-arm64@12.3.4':
+ optional: true
+
+ '@pnpm/exe.win32-x64@12.3.4':
+ optional: true
+
+ pnpm@12.3.4:
+ optionalDependencies:
+ '@pnpm/exe.darwin-arm64': 12.3.4
+ '@pnpm/exe.darwin-x64': 12.3.4
+ '@pnpm/exe.linux-arm64': 12.3.4
+ '@pnpm/exe.linux-arm64-musl': 12.3.4
+ '@pnpm/exe.linux-x64': 12.3.4
+ '@pnpm/exe.linux-x64-musl': 12.3.4
+ '@pnpm/exe.win32-arm64': 12.3.4
+ '@pnpm/exe.win32-x64': 12.3.4
+
+---
lockfileVersion: '9.0'
settings:
diff --git a/backend/pnpm-workspace.yaml b/backend/pnpm-workspace.yaml
index b3fbd9192b..3ebe73f82d 100644
--- a/backend/pnpm-workspace.yaml
+++ b/backend/pnpm-workspace.yaml
@@ -1,2 +1,4 @@
+storeDir: ../.pnpm-store
+
minimumReleaseAgeExclude:
- brace-expansion@5.0.8 || 5.0.9
diff --git a/backend/src/app/tasks/telemetry.clj b/backend/src/app/tasks/telemetry.clj
index 82696144db..7064c8f40c 100644
--- a/backend/src/app/tasks/telemetry.clj
+++ b/backend/src/app/tasks/telemetry.clj
@@ -17,7 +17,6 @@
[app.http.client :as http]
[app.main :as-alias main]
[app.setup :as-alias setup]
- [app.util.blob :as blob]
[app.util.json :as json]
[integrant.core :as ig]
[promesa.exec :as px]))
@@ -248,20 +247,16 @@
:props (or (some-> props db/decode-transit-pgobject) {})
:context (or (some-> context db/decode-transit-pgobject) {})}))
-(defn- encode-batch
- "Encode a sequence of event maps into a fressian+zstd base64 string
- suitable for JSON transport."
- ^String [events]
- (blob/encode-str events {:version 4}))
-
(defn send-event-batch
"Send a single batch of events to the telemetry endpoint. Returns
- true on success."
+ true on success. The events are sent as a plain vector of event
+ maps; the JSON encoder handles UUID and temporal types natively and
+ the receiver coerces them back to proper types."
[{:keys [::setup/props] :as cfg} batch]
(let [payload {:type :telemetry-events
:version (:full cf/version)
:instance-id (:instance-id props)
- :events (encode-batch batch)}
+ :events (vec batch)}
request {:method :post
:uri (cf/get :telemetry-uri)
:headers {"content-type" "application/json"}
diff --git a/backend/test/backend_tests/tasks_telemetry_test.clj b/backend/test/backend_tests/tasks_telemetry_test.clj
index e12af0553b..a44f6efe74 100644
--- a/backend/test/backend_tests/tasks_telemetry_test.clj
+++ b/backend/test/backend_tests/tasks_telemetry_test.clj
@@ -12,7 +12,6 @@
[app.db :as db]
[app.loggers.audit :as audit]
[app.tasks.telemetry :as telemetry]
- [app.util.blob :as blob]
[app.util.json :as json]
[backend-tests.helpers :as th]
[clojure.test :as t]
@@ -59,11 +58,6 @@
:cnt
long))
-(defn- decode-event-batch
- "Decode the base64+fressian+zstd event-batch sent to the mock."
- [b64-str]
- (blob/decode-str b64-str))
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; STATS / REPORT STRUCTURE TESTS (existing behaviour, extended)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -245,21 +239,19 @@
(t/is (not (contains? ev :ip-addr)))))))))
(t/deftest test-batch-encoding-is-decodable
- ;; Verify that encode-batch produces a blob that round-trips back
- ;; through blob/decode to the original data.
+ ;; Events are sent as a plain vector of raw event maps (no blob
+ ;; encoding): every batch must JSON round-trip unchanged, because
+ ;; the receiver coerces types from the plain JSON representation.
(let [events [{:name "navigate" :type "action" :source "telemetry"
:tracked-at (ct/now)}
{:name "create-file" :type "action" :source "telemetry"
:tracked-at (ct/now)}]
- ;; Call the private fn through the ns-mapped var
- encode (ns-resolve 'app.tasks.telemetry 'encode-batch)
- encoded (encode events)
- decoded (decode-event-batch encoded)]
- (t/is (string? encoded))
- (t/is (seq decoded))
- (t/is (= (count events) (count decoded)))
- (t/is (= "navigate" (:name (first decoded))))
- (t/is (= "create-file" (:name (second decoded))))))
+ encoded (json/encode-str {:events (vec events)})
+ decoded (json/decode encoded)]
+ (t/is (vector? (:events decoded)))
+ (t/is (= (count events) (count (:events decoded))))
+ (t/is (= "navigate" (:name (first (:events decoded)))))
+ (t/is (= "create-file" (:name (second (:events decoded)))))))
(t/deftest test-multiple-batches-when-many-events
;; Lower batch-size to 1 so that 3 events produce 3 separate
@@ -787,9 +779,13 @@
(t/is (= "telemetry-events" (name (:type body))))
(t/is (string? (:version body)))
(t/is (some? (:instance-id body)))
- ;; :events is a base64-encoded blob
- (t/is (string? (:events body)))
- (t/is (pos? (count (:events body))))))))))
+ ;; :events is a plain vector of raw event maps
+ (t/is (vector? (:events body)))
+ (t/is (pos? (count (:events body))))
+ (doseq [ev (:events body)]
+ (t/is (string? (:name ev)))
+ (t/is (string? (:source ev)))
+ (t/is (string? (:tracked-at ev))))))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TASK BRANCH COVERAGE
diff --git a/common/package.json b/common/package.json
index acc9432642..4269da7ba6 100644
--- a/common/package.json
+++ b/common/package.json
@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC Sucursal en España SL",
"private": true,
- "packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67",
+ "packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457",
"type": "module",
"repository": {
"type": "git",
diff --git a/common/pnpm-lock.yaml b/common/pnpm-lock.yaml
index 77f173a685..ef70c46056 100644
--- a/common/pnpm-lock.yaml
+++ b/common/pnpm-lock.yaml
@@ -7,96 +7,96 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
- specifier: 12.0.0
- version: 12.0.0
+ specifier: 12.3.4
+ version: 12.3.4
packages:
- '@pnpm/exe.darwin-arm64@12.0.0':
- resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
+ '@pnpm/exe.darwin-arm64@12.3.4':
+ resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
cpu: [arm64]
os: [darwin]
- '@pnpm/exe.darwin-x64@12.0.0':
- resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
+ '@pnpm/exe.darwin-x64@12.3.4':
+ resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
cpu: [x64]
os: [darwin]
- '@pnpm/exe.linux-arm64-musl@12.0.0':
- resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
+ resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-arm64@12.0.0':
- resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
+ '@pnpm/exe.linux-arm64@12.3.4':
+ resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-x64-musl@12.0.0':
- resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
+ '@pnpm/exe.linux-x64-musl@12.3.4':
+ resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-x64@12.0.0':
- resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
+ '@pnpm/exe.linux-x64@12.3.4':
+ resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.win32-arm64@12.0.0':
- resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
+ '@pnpm/exe.win32-arm64@12.3.4':
+ resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
cpu: [arm64]
os: [win32]
- '@pnpm/exe.win32-x64@12.0.0':
- resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
+ '@pnpm/exe.win32-x64@12.3.4':
+ resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
cpu: [x64]
os: [win32]
- pnpm@12.0.0:
- resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
+ pnpm@12.3.4:
+ resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
- '@pnpm/exe.darwin-arm64@12.0.0':
+ '@pnpm/exe.darwin-arm64@12.3.4':
optional: true
- '@pnpm/exe.darwin-x64@12.0.0':
+ '@pnpm/exe.darwin-x64@12.3.4':
optional: true
- '@pnpm/exe.linux-arm64-musl@12.0.0':
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
optional: true
- '@pnpm/exe.linux-arm64@12.0.0':
+ '@pnpm/exe.linux-arm64@12.3.4':
optional: true
- '@pnpm/exe.linux-x64-musl@12.0.0':
+ '@pnpm/exe.linux-x64-musl@12.3.4':
optional: true
- '@pnpm/exe.linux-x64@12.0.0':
+ '@pnpm/exe.linux-x64@12.3.4':
optional: true
- '@pnpm/exe.win32-arm64@12.0.0':
+ '@pnpm/exe.win32-arm64@12.3.4':
optional: true
- '@pnpm/exe.win32-x64@12.0.0':
+ '@pnpm/exe.win32-x64@12.3.4':
optional: true
- pnpm@12.0.0:
+ pnpm@12.3.4:
optionalDependencies:
- '@pnpm/exe.darwin-arm64': 12.0.0
- '@pnpm/exe.darwin-x64': 12.0.0
- '@pnpm/exe.linux-arm64': 12.0.0
- '@pnpm/exe.linux-arm64-musl': 12.0.0
- '@pnpm/exe.linux-x64': 12.0.0
- '@pnpm/exe.linux-x64-musl': 12.0.0
- '@pnpm/exe.win32-arm64': 12.0.0
- '@pnpm/exe.win32-x64': 12.0.0
+ '@pnpm/exe.darwin-arm64': 12.3.4
+ '@pnpm/exe.darwin-x64': 12.3.4
+ '@pnpm/exe.linux-arm64': 12.3.4
+ '@pnpm/exe.linux-arm64-musl': 12.3.4
+ '@pnpm/exe.linux-x64': 12.3.4
+ '@pnpm/exe.linux-x64-musl': 12.3.4
+ '@pnpm/exe.win32-arm64': 12.3.4
+ '@pnpm/exe.win32-x64': 12.3.4
---
lockfileVersion: '9.0'
diff --git a/common/pnpm-workspace.yaml b/common/pnpm-workspace.yaml
index a1c031fc33..7b3a5d3edf 100644
--- a/common/pnpm-workspace.yaml
+++ b/common/pnpm-workspace.yaml
@@ -1 +1,3 @@
+storeDir: ../.pnpm-store
+
minimumReleaseAge: 0
diff --git a/docs/img/objects/line-arrow-tools.webp b/docs/img/objects/line-arrow-tools.webp
new file mode 100644
index 0000000000..4615e65cb8
Binary files /dev/null and b/docs/img/objects/line-arrow-tools.webp differ
diff --git a/docs/package.json b/docs/package.json
index 7d498ee015..208cd18089 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -39,5 +39,5 @@
"markdown-it-anchor": "^9.2.1",
"markdown-it-plantuml": "^1.4.1"
},
- "packageManager": "pnpm@12.0.0+sha512.9e2e3dc3911995868dc94b8175c217c27e95408fa03b4a22749778f2b34f773b77cdd3b39ede8171b22fcd53be6a35342e9fac9948a68ef58df6488ce89a7e67"
+ "packageManager": "pnpm@12.3.4+sha512.961aa41fb077da3a04a441d9f8e15ebc0c96da8ef710b2eb67bf9ee7cb0610eabd48f1fd85f51cffe73846785fa0f87c56a3a872a1d893f8446741b5cce45457"
}
diff --git a/docs/pnpm-lock.yaml b/docs/pnpm-lock.yaml
index 5938203566..3527b910e8 100644
--- a/docs/pnpm-lock.yaml
+++ b/docs/pnpm-lock.yaml
@@ -7,96 +7,96 @@ importers:
configDependencies: {}
packageManagerDependencies:
pnpm:
- specifier: 12.0.0
- version: 12.0.0
+ specifier: 12.3.4
+ version: 12.3.4
packages:
- '@pnpm/exe.darwin-arm64@12.0.0':
- resolution: {integrity: sha512-sqeoPfVMIfQhbwzDrKraXY2ynyuWClFqzvfImzAS/yczEru1m5SGvQ9kgFPDvQzJZ9AetedgJeDZC6qYvH8/tQ==}
+ '@pnpm/exe.darwin-arm64@12.3.4':
+ resolution: {integrity: sha512-PAyUol8T1+/+ViOiXAt51ECA+QnfXCqz6foL4bW+LsoX0NcVd5XVEM2mRQu+LV4oc7uRz9zf9U0P+XFfuQeDAw==}
cpu: [arm64]
os: [darwin]
- '@pnpm/exe.darwin-x64@12.0.0':
- resolution: {integrity: sha512-Quc3J6c9cGTy+LDgz1cLVgCNOU9IERuyAlDoEj0DCilKqvo50Jx1GV8k74iwn4J9fFSKkm8JrwNvtTDj3uWnUA==}
+ '@pnpm/exe.darwin-x64@12.3.4':
+ resolution: {integrity: sha512-fxP9JCk0Cdye+ePuj+GJJLMUMTqHGWRdb1dtv4How876uQ2ehxvenpgiYAir/ceO9PsYUZkFTtyZdx+rRu5QOA==}
cpu: [x64]
os: [darwin]
- '@pnpm/exe.linux-arm64-musl@12.0.0':
- resolution: {integrity: sha512-EVWd3OTmgsMFhXx69b5JxIzoabG9Ma7m4OeTaf0ZKBzMnfYi8u21NDQo92ToMrdYL5dYDDCHsyYIjXzk+d0HhA==}
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
+ resolution: {integrity: sha512-FBOt0/7ye6O6q4AllVV5QMviB6qE6fqkeczV/+MDWQsmo+QJrlfsh6X7CpH/tClVpBZEyIbjpUoT8bNhCYBxEg==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-arm64@12.0.0':
- resolution: {integrity: sha512-cXHHW8M4rAPsYNkKZO9WVcpLLK55i9EaIsZPfIqUuY2eopd5LqnFyBge54HCh1GC0yCX8ySn0hYIi+4OyAEoDg==}
+ '@pnpm/exe.linux-arm64@12.3.4':
+ resolution: {integrity: sha512-t71AVA7LRqiKTyZ5xMYaZc2n5DfdpMbfokZuiIOXHBOM03ECnF0t4iYwaBDqJgVjlKYUOwaF/bRQajGNA4cJ4w==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.linux-x64-musl@12.0.0':
- resolution: {integrity: sha512-UcXwMdFjly0mpddkGigHKTxe27IMv2fUK4IWW/MHmJ3yMguxXmkwNlEI4aE+G1HO2TLo20uNEUWD4ymLe/DaCQ==}
+ '@pnpm/exe.linux-x64-musl@12.3.4':
+ resolution: {integrity: sha512-RPmk7Jb/aYaFvL2iyDN/AtMY+hUEsue732WmXpcuQ9tBpMnGyA5py7Z3+e+qmQaJ0zY/4ni9jJiyPBQHujmv6w==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@pnpm/exe.linux-x64@12.0.0':
- resolution: {integrity: sha512-6Rsl+zEWMOmus7v7/9J3OE8EMvHyNAfxYmDfmhQG4J0985OuT3G3Ho9NSGHjkBn4aU4bgklWifRhe1HX8dUSyw==}
+ '@pnpm/exe.linux-x64@12.3.4':
+ resolution: {integrity: sha512-2ZqOlSPkfwX1h5cR+FPiWf8+F+2hZT/3TvhUK5sigHqwaQCIiq8R7CGxhndKs63JtcLi2a1Qpo+wX/EoyfjyJQ==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@pnpm/exe.win32-arm64@12.0.0':
- resolution: {integrity: sha512-O5F76A4oVFrpDGdFxEszRIThOSBfjHdH5c006gR+7UTCfiXrukr1XfqPungUI1DXcSR5gb9jBsPQqQZOAoOOxw==}
+ '@pnpm/exe.win32-arm64@12.3.4':
+ resolution: {integrity: sha512-ANyrHqyqco6SXBysUTRF74itDyyraea7IbFsKFdNXTjcFnfycTDx37EwuhdpPYFNSIh2JhUG4fByclsRfiHX7w==}
cpu: [arm64]
os: [win32]
- '@pnpm/exe.win32-x64@12.0.0':
- resolution: {integrity: sha512-5dKFajIEWJ1ai+KHXFJvskY6vchbunmLwSUV2ywbLymcmJjfY5XJVpgzPCyIoVCMVG0zHorr66+hM8h8b3aRfQ==}
+ '@pnpm/exe.win32-x64@12.3.4':
+ resolution: {integrity: sha512-WH/KqBPY/hq2Tb7SgQltEZytimcjgKRaCRL/aM9CI0c67iKc5TVmHUhIiL3Ux9FB4bWn36i6XewUcScQI+zG8w==}
cpu: [x64]
os: [win32]
- pnpm@12.0.0:
- resolution: {integrity: sha512-ni49w5EZlYaNyUuBdcIXwn6VQI+gO0oidJd48rNPdzt3zdOznt6BcbIvzVO+ajU0Lp+smUimjvWN9kiM6Jp+Zw==}
+ pnpm@12.3.4:
+ resolution: {integrity: sha512-lhqkH7B32joEpEHZ+OFevAyW2o73ELLrZ7+e58sGEOq9SPH9hfUc/+c4RnhfoPh8VqOocqHYk/hEZ0G1zORUVw==}
engines: {node: '>=18.*'}
hasBin: true
snapshots:
- '@pnpm/exe.darwin-arm64@12.0.0':
+ '@pnpm/exe.darwin-arm64@12.3.4':
optional: true
- '@pnpm/exe.darwin-x64@12.0.0':
+ '@pnpm/exe.darwin-x64@12.3.4':
optional: true
- '@pnpm/exe.linux-arm64-musl@12.0.0':
+ '@pnpm/exe.linux-arm64-musl@12.3.4':
optional: true
- '@pnpm/exe.linux-arm64@12.0.0':
+ '@pnpm/exe.linux-arm64@12.3.4':
optional: true
- '@pnpm/exe.linux-x64-musl@12.0.0':
+ '@pnpm/exe.linux-x64-musl@12.3.4':
optional: true
- '@pnpm/exe.linux-x64@12.0.0':
+ '@pnpm/exe.linux-x64@12.3.4':
optional: true
- '@pnpm/exe.win32-arm64@12.0.0':
+ '@pnpm/exe.win32-arm64@12.3.4':
optional: true
- '@pnpm/exe.win32-x64@12.0.0':
+ '@pnpm/exe.win32-x64@12.3.4':
optional: true
- pnpm@12.0.0:
+ pnpm@12.3.4:
optionalDependencies:
- '@pnpm/exe.darwin-arm64': 12.0.0
- '@pnpm/exe.darwin-x64': 12.0.0
- '@pnpm/exe.linux-arm64': 12.0.0
- '@pnpm/exe.linux-arm64-musl': 12.0.0
- '@pnpm/exe.linux-x64': 12.0.0
- '@pnpm/exe.linux-x64-musl': 12.0.0
- '@pnpm/exe.win32-arm64': 12.0.0
- '@pnpm/exe.win32-x64': 12.0.0
+ '@pnpm/exe.darwin-arm64': 12.3.4
+ '@pnpm/exe.darwin-x64': 12.3.4
+ '@pnpm/exe.linux-arm64': 12.3.4
+ '@pnpm/exe.linux-arm64-musl': 12.3.4
+ '@pnpm/exe.linux-x64': 12.3.4
+ '@pnpm/exe.linux-x64-musl': 12.3.4
+ '@pnpm/exe.win32-arm64': 12.3.4
+ '@pnpm/exe.win32-x64': 12.3.4
---
lockfileVersion: '9.0'
diff --git a/docs/pnpm-workspace.yaml b/docs/pnpm-workspace.yaml
index bd2aa47620..b9b659dcc5 100644
--- a/docs/pnpm-workspace.yaml
+++ b/docs/pnpm-workspace.yaml
@@ -1,3 +1,5 @@
+storeDir: ../.pnpm-store
+
minimumReleaseAgeExclude:
- undici@7.28.0 || 7.29.0
- js-yaml@3.15.0 || 4.3.0
diff --git a/docs/user-guide/account-teams/projects-files.njk b/docs/user-guide/account-teams/projects-files.njk
index 02f1afd38c..fa3029e7c9 100644
--- a/docs/user-guide/account-teams/projects-files.njk
+++ b/docs/user-guide/account-teams/projects-files.njk
@@ -56,6 +56,10 @@ desc: Learn how to organize your work in Penpot. Create, manage and organize pro
When creating a file, you'll be asked to give it a name. The file will open in the workspace where you can start designing immediately.
+
Grid and list view
+
In the dashboard header you can switch how files are shown: a thumbnail Grid view or a compact List view. The choice is kept in the browser and applies both to the team files view and inside a project.
+
List view shows the file name, whether the file is a shared library, the last modification time, and the same options menu as the file cards.
+
Edit a file
To rename a file, right-click on the file card in the dashboard and select Rename, or click on the three-dot menu on the file card. Enter the new name and confirm the change. You can also access file settings and other options from the file's context menu.
diff --git a/docs/user-guide/designing/color-stroke.njk b/docs/user-guide/designing/color-stroke.njk
index 012b45b264..bd1cab3000 100644
--- a/docs/user-guide/designing/color-stroke.njk
+++ b/docs/user-guide/designing/color-stroke.njk
@@ -122,8 +122,14 @@ desc: Style your designs with Penpot's options! Learn about color fills, gradien
+
Stroke to path
+
A path is the underlying geometry: a sequence of points and segments (straight lines or Bezier curves). A stroke is the visible line drawn along that path, with properties such as width, color, opacity, dashes, caps, and joins.
+
Stroke to path turns the visible outline of a stroke into a separate, editable path. You can then reshape or style that outline independently of the original path. The new shape is usually closed. That matters for SVG, boolean operations, and editing the outline as geometry.
+
To convert a stroke, select a layer that has one, open the layer menu, and choose Stroke to path. Penpot creates a new path from each stroke (named with " (stroke)") and removes the strokes from the original layer.
+
Stroke to path is currently available when WebGL rendering is enabled.
+
Stroke Caps
-
Ever needed an arrow to point something? You can style the ends of any open paths selecting different styles for each end of an open path.
+
Ever needed an arrow to point to something? You can style the ends of any open paths selecting different styles for each end of an open path. You can also start from the Arrow tool, which already applies a triangle cap.
Text
Text layers are how you add copy to your designs in Penpot. If you want to go deeper into fonts, typography and advanced text options, check the dedicated Text & Typography section.
+
Lines and arrows
+
Use the Line and Arrow tools to draw a two-point path in one drag. They live in the shapes flyout of the toolbar, together with rectangle and ellipse.
+
+
Line (L): click and drag. Hold Shift/⇧ to snap the angle in 15 degree increments.
+
Arrow: the same gesture, with a triangle arrow cap on the end. Pick it from the shapes flyout. You can change or remove the cap from the stroke caps.
+
+
The result is a regular path, so you can edit its nodes and stroke like any other path.
+
+
+
+
Curves (freehand)
The curve tool allows a path to be created directly in a freehand mode.
-Select the curve tool by clicking on the icon at the toolbar or pressing Shift/⇧ + c.
+Select the curve tool from the free-draw flyout in the toolbar or press Shift/⇧ + C.
The path created will contain a lot of points, but it is edited the same way as any other curve.
Paths (bezier)
-
A path is composed of two or more nodes and the line segments between them, which may also be curved. To draw a new path you have to select the path tool by clicking on the icon at the toolbar or pressing P. Then you have two ways to create the path:
+
A path is composed of two or more nodes and the line segments between them, which may also be curved. To draw a new path, select the path tool from the free-draw flyout in the toolbar or press P. Then you have two ways to create the path:
Click to create a new corner node.
Click and drag to create a curved node.
@@ -211,7 +222,7 @@ You can choose to edit individual nodes or create new ones. Press Esc
Layer actions
Create
-
To create a layer you have to select the type of layer by clicking the selected tool (board, rectangle, ellipse, text, image, path or curve) at the toolbar. Then you usually have to click and drag your mouse on the viewport.
+
To create a layer, pick a tool from the toolbar. Board, text, and image stay on the toolbar. Rectangle, ellipse, line, and arrow are in the shapes flyout. Path and curve are in the free-draw flyout. Then you usually click and drag on the viewport.
Hold Shift/⇧ while creating an ellipse or a rectangle to maintain equal width and height.
diff --git a/docs/user-guide/designing/text-typo.njk b/docs/user-guide/designing/text-typo.njk
index 585a11b7cb..3317821ff2 100644
--- a/docs/user-guide/designing/text-typo.njk
+++ b/docs/user-guide/designing/text-typo.njk
@@ -32,7 +32,7 @@ desc: Penpot's guide on custom fonts! Upload, manage, and use custom fonts in Pe
-
Font family. Penpot includes by default the Google Fonts cataloge. You can also install your own fonts. In the font selector, each family name is shown in its own typeface so you can compare fonts before applying one. The name is the preview. While a font loads, you may briefly see the interface font instead. The preview uses a default variant, not every weight or style of the family.
In the Pages panel you can select more than one page and delete them in a single action.
+
+
Click a page to open it. This selects only that page.
+
Shift/⇧ + click to select a range of pages. This does not change the page you are viewing.
+
Ctrl/⌘ + click to add or remove one page from the selection. This does not navigate.
+
+
Right-click a selected page and choose Delete pages. Confirm to remove all selected pages in one step. A file always keeps at least one page. Page separators cannot be included in the selection.
+
Page separators
You can add visual dividers in the page list to group pages without extra structure. Create an empty page, then rename it to ---. The page appears as a horizontal line in the list.
A page only becomes a separator if its name is --- and the page has no content on the canvas. A page with content that is named --- stays a normal page. Separators cannot be opened or selected; you can reorder or delete them like other pages.
diff --git a/docs/user-guide/first-steps/shortcuts.njk b/docs/user-guide/first-steps/shortcuts.njk
index 76b323ec31..b06057a9b9 100644
--- a/docs/user-guide/first-steps/shortcuts.njk
+++ b/docs/user-guide/first-steps/shortcuts.njk
@@ -681,8 +681,8 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
Curve
-
CtrlC
-
⌘C
+
ShiftC
+
⇧C
Ellipse
@@ -694,6 +694,11 @@ desc: Get quickstart tips, shortcuts, and tutorials for Penpot! Learn interface
ShiftK
⇧K
+
+
Line
+
L
+
L
+
Path
P
diff --git a/docs/user-guide/first-steps/the-interface.njk b/docs/user-guide/first-steps/the-interface.njk
index 570087d6e8..c670c0f6c6 100644
--- a/docs/user-guide/first-steps/the-interface.njk
+++ b/docs/user-guide/first-steps/the-interface.njk
@@ -41,9 +41,9 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
Viewport: An infinite canvas where you can design without limits.
-
Toolbar: This is where you’ll find all the tools to quickly and easily create different types of layers: board, rectangle, ellipse, text, graphic, path, and free drawing. Learn more about layers.
+
Toolbar: Tools to create layers. Board, text, and image stay on the toolbar. Shapes (rectangle, ellipse, line, and arrow) and free-draw tools (path and curve) are grouped in flyouts. Open a flyout to pick a tool, or use its shortcut. Learn more about layers.
Main menu: From the main menu, you can customize your workspace. Manage the visibility of grids, rulers, and panels. Enable or disable snapping and dynamic alignment. Add or remove the file as a Shared Library. You’ll also find help resources here.
-
Pages: A file can contain as many pages as you need. Each page has its own viewport (the almost infinite area where you design) and its own layers. You can create, delete, or reorder pages as needed.
+
Pages: A file can contain as many pages as you need. Each page has its own viewport (the almost infinite area where you design) and its own layers. You can create, delete, or reorder pages, and select several pages at once to delete them. More about pages.
Layers: Layers are the different objects you can place in the design viewport. More about Layers panel.
Rulers: Rulers provide coordinates to help you design. You can also drag guides from them.
Color palette: The color palette gives you quick access to a visible library of colors. Use the menu to easily switch between libraries. Learn more about the color palette..
@@ -137,7 +137,7 @@ desc: Discover Penpot's free user guide! Learn the interface, workspace basics,
User area: This must be you! Access your profile settings, Penpot tutorials, the Penpot Community and more. You can also find here a way to leave us feedback. We’d love to read your thoughts :).
Comments notifications: Here you will be able to see if you have unread comments inside the files of the team. There's also a button to mark all notifications as read.
Create project: Create as many projects as you need to organize your designs.
-
File card: Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete).
+
File card: Basic information about a file at plain sight. A preview, update info or if it’s added as a Shared Library. From there you can perform several actions over the file (rename, duplicate, move, download, delete). You can also switch the files area between a thumbnail grid and a compact list. More about grid and list view.
Libraries & Templates module: A curated selection of Libraries & Templates files ready to import.