Reviewer follow-up on PR #9151. The "Delete group" handler was
duplicated across the three assets-panel sections (colors,
typographies, components), each carrying the same skeleton — filter
by group path, build an undo-id, run the deletes inside one
transaction, and show the same confirm modal — with only the path
predicate and the per-asset delete event differing.
Add `app.main.ui.workspace.sidebar.assets.common/make-delete-asset-group-fn`
that takes the differing parts as options:
- `:assets` collection to filter.
- `:on-clear-selection` invoked before the deletes.
- `:delete-events` `(fn [matching-assets] => seq-of-events)`.
- `:path-filter` predicate (defaults to `str/starts-with?`),
overridden to `cpn/inside-path?` for
components so nested group paths match the
same way the existing ungroup/combine helpers
do.
The factory returns `(fn [path] …)` so each call site stays a
straight `mf/use-fn`. The variant-container dedup in components
(one `delete-shapes` per container, not one per sibling variant)
moves into that section's `:delete-events` fn and is unchanged in
behavior.
Cleanup
-------
The `:as i18n` alias is no longer needed in any of the three section
files (its only use was `i18n/c` for the modal count, which the
helper now handles); reduced to `:refer [tr]`.
Github #9141
Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
The height comparison in frame-same-size? used the misspelled keyword
:heigth on both sides. (:heigth selrect) returns nil for any selrect,
so (= nil nil) is always true and the function degenerated to a width-only
comparison.
Result: the 'paste next to selected frame' branch in clipboard.cljs fired
whenever pasted-content width matched a target frame's width, even if the
heights differed.
Introduced in #9033 (✨ Add paste to replace (Cmd+Shift+V)).
Signed-off-by: iot2edge <tylerprice830@gmail.com>
Co-authored-by: iot2edge <tylerprice830@gmail.com>
Rename 5 deprecated mixin calls from camelCase to kebab-case to
match the actual mixin names defined in common-refactor.scss:
- deprecated.flexCenter -> deprecated.flex-center
- deprecated.headlineSmallTypography -> deprecated.headline-small-typography
- deprecated.headlineLargeTypography -> deprecated.headline-large-typography
- deprecated.bodyLargeTypography -> deprecated.body-large-typography
- deprecated.bodyMediumTypography -> deprecated.body-medium-typography
This fixes the "Undefined mixin" SCSS compilation error.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Introduce a new OpenCode workflow skill that guides users through
backporting commits by applying diffs instead of using cherry-pick.
This is useful when cherry-pick is undesirable (e.g. divergent
histories, binary conflicts, or partial porting).
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Two coupled defects made shape.applyToken(), token.applyToShapes() and
token.applyToSelected() silently no-op when invoked from JavaScript with
an array of strings (e.g. token.applyToShapes([rect], ["fill"])):
1. token-attr-plugin->token-attr only consulted its alias map when the
input was already a keyword; string inputs fell through unchanged,
causing downstream token-attr? to return false.
2. The inner schemas used plain [:set ...] which lacks the :decode/json
transformer for JS array -> Clojure set coercion. Switching to
Penpot's custom [::sm/set ...] lets the standard JSON decoder
pipeline handle the conversion automatically.
This is a backport of commit 1eac3e2be5f973359ad2ec9bac4e80a9d5a9e022
which fixes GitHub #9162.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Improve MCP instructions on design creation:
* Agents should make use of layouts when appropriate
* Agents should name all elements appropriately
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
* 🐛 Preserve renamed layer name when re-entering edit mode
When a layer was renamed and the user clicked its name again to edit
it, the input opened with the type-based default name instead of the user's saved name. Pressing Enter then
silently overwrote the saved name with the default. Read the current
shape :name when seeding the rename input so the user's previous
rename is preserved.
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 💄 Remove redundant DOM-refresh effect from layer rename input
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
---------
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
* ✨ Show specific error messages for invitation token failures
Surface distinct error messages for the three invitation-token failure
modes that the backend already distinguishes: email mismatch, expired
token, and invalid/corrupted token. Replaces the single generic
could not accept invitation message with actionable text so the
user knows what went wrong and how to recover.
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 💄 Update CHANGE.md
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 💄 Address review feedback on invitation-error messages
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
---------
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Explicitly state that the team is small and reviews may take a
few days as they are handled in dedicated time blocks.
Remove mention of GitHub Discussions since it is not used.
Reword the discussion requirement to manage expectations: do not
expect a PR to be accepted without prior discussion.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
Rename subscription.settings.sucess.dialog.{title,footer} to
subscription.settings.success.dialog.{title,footer} in en.po and
update the three callsites in subscription.cljs.
Closes#9203
Signed-off-by: jack-stormentswe <crazycoder131@gmail.com>
* 🐛 Fix Plugin API token application for JS array of strings
Plugin code calling `shape.applyToken(token, ["fill"])` or
`token.applyToShapes([rect], ["fill"])` from JavaScript supplies a JS
array of strings. The plugin proxies expected a Clojure set of
keywords, and two coupled defects made the calls silently no-op (or,
with `throwValidationErrors` enabled, throw "check error"):
1. `token-attr-plugin->token-attr` only consulted its alias map when
the input was already a keyword. String inputs like "fill" fell
through to the identity branch, so the downstream
`cto/token-attr?` predicate (which checks against a set of
keywords) returned false for every string. Coerce strings to
keywords first.
2. The `applyToken` / `applyToShapes` / `applyToSelected` schemas
used plain `[:set ...]`, which has no `:decode/json` transformer
for JS array → Clojure set coercion. Switch to the registered
`[::sm/set ...]` (in `app.common.schema`) which provides the
array → set decoder. After the switch, the standard JSON pipeline
converts `["fill"]` to `#{"fill"}`, then the inner
`[:and ::sm/keyword [:fn token-attr?]]` decodes each element to a
keyword and validates it.
Also extends the docstring on `token-attr-plugin->token-attr` to make
the string-friendly contract explicit, and registers a new
`tokens-test` ns under `frontend/test/frontend_tests/plugins/` with
six `deftest` blocks covering:
- known keywords passing through unchanged
- keyword aliases (`:r1` → `:border-radius-top-left`, etc.)
- string inputs coerced to keywords (regression for #9162)
- `token-attr?` accepting both keyword and string inputs
- `token-attr?` rejecting unknown attrs and nil
Closes#9162
* 🐛 Fix wrong direction in plugin-name alias tests
The added tests in tokens_test.cljs and the new docstring in tokens.cljs
described the alias resolution in the wrong direction. The map is
{:r1 :border-radius-top-left, …} then map-invert'd, so
token-attr-plugin->token-attr maps verbose plugin-side names
(:border-radius-top-left) to canonical internal short names (:r1),
not the other way around. Inputs already in canonical form (:r1, :fill,
"fill", …) pass through unchanged. Flipped the alias-resolution test
expectations and the keyword/string-input cases, refreshed the docstring
and the regression-coverage comment to match.
---------
Co-authored-by: Andrey Antukh <niwi@niwi.nz>
Combine margin-block-start and margin-block-end into the margin-block
shorthand to satisfy the declaration-block-no-redundant-longhand-properties
stylelint rule.
Signed-off-by: Andrey Antukh <niwi@niwi.nz>
When working with large asset groups, users asked for a one-click way
to remove every asset under a group path. Multi-select across hundreds
of items is impractical, and ungrouping first and then deleting leaves
the orphaned items in the flat list.
This change adds a "Delete group" option to the assets-panel
context-menu for three asset types that already carry group structure:
- Components (including variants — sibling variants sharing a variant
container are deduplicated, and the container is deleted once via
the same dispatch the per-item delete uses in file_library.cljs).
- Colors.
- Typographies.
A confirmation modal is shown before deletion, with the count of
assets to be removed, so the action is never silent. All deletes run
inside a single undo transaction, so one Cmd+Z restores the whole
group.
Changes
-------
- `assets/groups.cljs` — `asset-group-title*` accepts an optional
`on-delete-group` prop and conditionally adds the menu entry
between "Ungroup" and "Combine as variants". When the callback is
not supplied the option is hidden, so asset sections that do not
implement it stay unaffected.
- `assets/components.cljs` — threads `on-delete-group` through the
recursive `components-group*` and defines the section-level
handler, dispatching to `dwsh/delete-shapes` for variant containers
and `dwl/delete-component` for plain components.
- `assets/colors.cljs` — same threading + a simple `dwl/delete-color`
dispatch per color in the group.
- `assets/typographies.cljs` — same threading + a
`dwl/delete-typography` dispatch per typography in the group.
- `translations/en.po` — three new strings: the menu label
(`workspace.assets.delete-group`) and the modal title/message
(`modals.delete-asset-group.title`/`.message`, plural-aware).
Github #9141
Signed-off-by: FairyPigDev <luislee3108@gmail.com>
Signed-off-by: FairyPiggyDev <luislee3108@gmail.com>
The ::mf/props and ::mf/wrap-props metadata keys are no-ops on modern
components (those defined with mf/defc and the * suffix) since the *
suffix already triggers the props behavior these keys attempt to
configure. This cleanup removes the redundant metadata from modern
components across all UI directories.
Changes:
- comments/: comments
- dashboard/: comments, deleted, files, fonts, grid, import, libraries,
pin_button, projects, search, sidebar, subscription, team, templates
- exports/: files
- modal/: modal
- settings/: subscription
- static/: static
- viewer/: comments, interactions, viewer
- workspace/: context_menu, libraries, sidebar/assets,
viewport/gradients, tokens/settings/menu