* 🐛 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.
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.
* 🐛 Reject duplicate chunk index in chunked uploads
Repeat uploads of the same chunk index each stored a new
object because upload-chunk only checked index bounds. Run the
handler in a transaction, lock the session row and reject an
already-stored index with :duplicate-chunk-index.
Also harden assemble-chunks to require exactly indices 0..n-1
so gaps or duplicates fail instead of assembling a corrupt
file. Covers media, fonts and binfile through the shared
helper.
Closes#11634
AI-assisted-by: muse-spark-1.3-contributor
* ✨ Cap upload chunk size at 30 MiB by default
Chunks were only bounded by the 350 MiB HTTP body limit while the
30 MiB caps applied to the assembled file. Add :upload-max-chunk-size
(default 30 MiB, tunable via env) and reject oversize chunks in
upload-chunk with :validation/:chunk-too-large before anything is
stored. App clients slice at 25/10 MiB, so no frontend change needed.
AI-assisted-by: muse-spark-1.3-contributor
* 🐛 Fix tx-run! call and storage resolve in upload-chunk
Pass cfg as first arg to db/tx-run!, which expects [system f & params; without it every chunk upload raised invalid system/cfg provided and no chunk was stored, breaking assemble with missing-chunks. Also resolve storage without reuse-conn: put-object! writes to the backend outside any transaction, so reusing the tx connection gives no atomicity. Media, font and storage suites green, lint and format clean. AI-assisted-by: muse-spark-1.3-contributor
Add a body line-length validator to scripts/check-commit. It
fails when a body line exceeds 76 characters, exempting
trailers, URLs, and unbreakable tokens. The 76 limit leaves
room for git log's four-space indent in an 80-column
terminal.
Align the subject limit with the documented 70 characters;
the checker allowed 90 before.
Document the rule as a hard, verifiable requirement in
AGENTS.md, CONTRIBUTING.md, the create-commit skill, and
the workflow memory, and point at scripts/check-commit.
Add tests for the validator and the subject length rule.
AI-assisted-by: deepseek-flash
AA clip on the Current→atlas blit softens shared tile edges so
the canvas background shows through as 1px lines at the 512px
grid when tiles are composed with SrcOver.
Closes#11638
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Move build-docker and build-docker-devenv jobs from penpot-extended-runner
to penpot-standar-runner, point tests-exporter at the canonical
penpot-extended-runner label instead of the stale penpot-runner-02 alias,
and switch build-tag/release notify jobs from ubuntu-24.04 to ubuntu-latest.
Signed-off-by: David Barragán Merino <david.barragan@kaleidos.net>
Cached Skia paragraphs bake absolute image/gradient shaders at layout
time. On move, clones reuse those paragraphs while painting at the new
selrect, so glyphs move and the fill stays put. Record the paint origin
when layout is built and translate the canvas when painting from cache
so shaders track the text. Also sync bounds before update_layout so
fills bake against the current container.
Rewrite sql:file-comment-users to join comment with
comment_thread and union the requesting profile id, then
join the resulting small id set against profile.
The previous "id IN (subquery) OR id = ?" forced a
sequential scan over the whole profile table with a hashed
subplan filter, taking ~1.9s on large instances. The
semi-join lets the planner use profile_pkey, dropping the
query to sub-millisecond time. UNION (not UNION ALL) keeps
the previous dedup semantics when the requesting profile is
also a commenter.
AI-assisted-by: deepseek-flash
* ♻️ 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
Fix LDAP injection vulnerability (T5-N1-03) where the client-supplied email was used directly in the LDAP search filter without escaping RFC 4515 special characters (*, (, ), \, NUL), and the profile email was taken from client input instead of the LDAP directory attribute.
Changes:
- Add escape-ldap-filter-value per RFC 4515 section 3
- Apply escaping in search-user before building LDAP filter
- Add get-attr helper for multi-valued LDAP attributes
- Fix retrieve-user to use directory email (attrs-email) instead of client email
- Use cuerdas.core instead of clojure.string
Closes#11084
AI-assisted-by: mimo-v2.5-pro
SkSVGDevice drops paint image-filters, so re-emit visible layer blur as a
native feGaussianBlur filter on the composite <g>. Match canvas sigma via
radius_to_sigma(value * scale), and skip Skia blur filters on the SVG
VectorRenderer path so shapes do not vanish.
Closes#11380
* 🐛 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>
Closes#11384
Skia's SVG backend drops save_layer+SrcIn, so image strokes are re-emitted
as a linked <image> clipped to an opaque stroke silhouette (filled outline,
clip-rule evenodd). Open-path caps join the silhouette and grow the image
dest by cap_bounds_margin so markers stay textured.
Added a new section for Penpot Enterprise detailing its features and benefits for organizations.
Signed-off-by: Elenzakaleidos <elena.scilinguo@kaleidos.net>
* ✨ 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>
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>
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.
* 🎉 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>
Session tokens now carry an :exp claim anchored to created-at (not
modified-at), so activity cannot extend the session beyond the
absolute maximum (default 30 days, configurable via
PENPOT_AUTH_TOKEN_COOKIE_MAX_AGE_ABSOLUTE). The existing token
verification already rejects expired tokens, so enforcement is
automatic. Also extends the GC task to purge expired
http_session_v2 rows, which were previously never cleaned up.
Closes#11444
AI-assisted-by: longcat-2.0
Drop the ignored-since-pnpm-11 onlyBuiltDependencies entry from
render-wasm/pnpm-workspace.yaml, keeping allowBuilds as the single
source of build approvals. Clarify the updating-pnpm gotcha so it no
longer claims pnpm writes ignoredBuiltDependencies.
AI-assisted-by: muse-spark-1.3-contributor
Teach agents to verify their changes with ./scripts/ci: module list,
task selection flags, log locations under .ci-logs/, and per-module
workflows (lint-only pass, --fix, --paren-repair, common/ consumer
checks). Register the skill in the skills README, point AGENTS.md at
the new memory, and add the script to the critical-info dev scripts.
AI-assisted-by: omen-alpha
The flow only supported continue mode (existing branch) or standalone
mode (issue + branch). Add a "direct" argument (aliases: "no branch",
"direct commit") that skips both and commits on the current branch as
it is, even when it is a base branch — for small tooling changes the
user wants committed in place.
AI-assisted-by: omen-alpha