Add visibility toggle for strokes (#8913)

*  Add visibility toggle for strokes

* ♻️ Use single emit! call for stroke visibility toggle

* 💄 Disable stroke controls when hidden, matching shadow/blur pattern

When a stroke is hidden, the alignment/style selects, cap selects, and
cap switch button are now disabled. A .hidden CSS class dims the
options area with reduced opacity. This matches the existing behavior
in shadow_row and blur menu where controls are disabled when the
effect is hidden.

* 💄 Move stroke hide button before remove button

---------

Signed-off-by: eureka928 <meobius123@gmail.com>
This commit is contained in:
Dream 2026-04-14 04:08:13 -04:00 committed by GitHub
parent 9106a994f1
commit 4703fe6e3b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 121 additions and 56 deletions

View File

@ -29,6 +29,7 @@
- Add per-group add button for typographies (by @eureka928) [Github #5275](https://github.com/penpot/penpot/issues/5275)
- Use page name for multi-export ZIP/PDF downloads (by @Dexterity104) [Github #8773](https://github.com/penpot/penpot/issues/8773)
- Make links in comments clickable (by @eureka928) [Github #1602](https://github.com/penpot/penpot/issues/1602)
- Add visibility toggle for strokes (by @eureka928) [Github #7438](https://github.com/penpot/penpot/issues/7438)
### :bug: Bugs fixed

View File

@ -145,7 +145,8 @@
[::sm/one-of stroke-caps]]
[:stroke-color {:optional true} clr/schema:hex-color]
[:stroke-color-gradient {:optional true} clr/schema:gradient]
[:stroke-image {:optional true} clr/schema:image]])
[:stroke-image {:optional true} clr/schema:image]
[:hidden {:optional true} :boolean]])
(def stroke-attrs
"A set of attrs that corresponds to stroke data type"

View File

@ -509,7 +509,8 @@
(when (some? shape-strokes)
[:> :g props
(for [[index value] (reverse (d/enumerate shape-strokes))]
(for [[index value] (reverse (d/enumerate shape-strokes))
:when (not (:hidden value))]
[:& shape-custom-stroke {:shape shape
:stroke value
:index index

View File

@ -12,6 +12,7 @@
[app.common.types.stroke :as cts]
[app.main.data.workspace :as udw]
[app.main.data.workspace.colors :as dc]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]]
@ -155,6 +156,13 @@
(st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (dc/change-stroke-attrs ids {:stroke-cap-start stroke-cap-end
:stroke-cap-end stroke-cap-start} index)))))
on-toggle-visibility
(mf/use-fn
(mf/deps ids)
(fn [index]
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwsh/update-shapes ids #(update-in % [:strokes index :hidden] not)))))
on-add-stroke
(fn [_]
(st/emit! (udw/trigger-bounding-box-cloaking ids))
@ -226,6 +234,7 @@
:applied-tokens (when (= 0 index) applied-tokens)
:on-detach-token on-detach-token
:on-remove on-remove
:on-toggle-visibility on-toggle-visibility
:on-reorder handle-reorder
:disable-drag disable-drag
:on-focus on-focus

View File

@ -40,6 +40,7 @@
on-stroke-cap-start-change
on-stroke-cap-end-change
on-stroke-cap-switch
on-toggle-visibility
disable-drag
on-focus
on-blur
@ -49,7 +50,9 @@
select-on-focus
ids]}]
(let [token-numeric-inputs
(let [hidden? (:hidden stroke)
token-numeric-inputs
(features/use-feature "tokens/numeric-input")
on-drop
@ -182,10 +185,18 @@
on-cap-switch
(mf/use-fn
(mf/deps index on-stroke-cap-switch)
#(on-stroke-cap-switch index))]
#(on-stroke-cap-switch index))
on-toggle-visibility
(mf/use-fn
(mf/deps index on-toggle-visibility)
(fn []
(when on-toggle-visibility
(on-toggle-visibility index))))]
[:div {:class (stl/css-case
:stroke-data true
:hidden hidden?
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))
:aria-label (str "stroke-row-" index)}
@ -195,22 +206,33 @@
;; Stroke Color
;; FIXME: memorize stroke color
[:> color-row* {:color (ctc/stroke->color stroke)
:index index
:title title
:on-change on-color-change-refactor
:on-detach on-color-detach
:on-remove on-remove
:disable-drag disable-drag
:applied-token (if (= index 0)
stroke-color-token
nil)
:on-detach-token on-detach-token-color
:on-token-change on-token-change
:on-focus on-focus
:origin :stroke-color
:select-on-focus select-on-focus
:on-blur on-blur}]
[:div {:class (stl/css :stroke-color-actions)}
[:> color-row* {:color (ctc/stroke->color stroke)
:index index
:title title
:on-change on-color-change-refactor
:on-detach on-color-detach
:disable-drag disable-drag
:applied-token (if (= index 0)
stroke-color-token
nil)
:on-detach-token on-detach-token-color
:on-token-change on-token-change
:on-focus on-focus
:origin :stroke-color
:select-on-focus select-on-focus
:on-blur on-blur}]
(when (some? on-toggle-visibility)
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.stroke.toggle-stroke")
:on-click on-toggle-visibility
:icon (if hidden? "hide" "shown")}])
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.options.stroke.remove-stroke")
:on-click on-remove
:icon i/remove}]]
;; Stroke Width, Alignment & Style
(if token-numeric-inputs
@ -230,6 +252,7 @@
:options stroke-alignment-options
:variant "icon-only"
:data-testid "stroke.alignment"
:disabled hidden?
:wrapper-class (stl/css :stroke-align-icon-select)
:on-change on-alignment-change}]
@ -239,6 +262,7 @@
:wrapper-class (stl/css :stroke-style-icon-select)
:data-testid "stroke.style"
:variant "icon-only"
:disabled hidden?
:dropdown-alignment :right
:on-change on-style-change}])]
@ -258,6 +282,7 @@
:data-testid "stroke.alignment"}
[:& select {:default-value stroke-alignment
:options stroke-alignment-options
:disabled hidden?
:on-change on-alignment-change}]]
(when-not disable-stroke-style
@ -265,6 +290,7 @@
:data-testid "stroke.style"}
[:& select {:default-value stroke-style
:options stroke-style-options
:disabled hidden?
:on-change on-style-change}]])])
;; Stroke Caps
@ -272,11 +298,14 @@
[:div {:class (stl/css :stroke-caps-options)}
[:& select {:default-value (:stroke-cap-start stroke)
:options stroke-caps-options
:disabled hidden?
:on-change on-caps-start-change}]
[:> icon-button* {:variant "secondary"
:aria-label (tr "labels.switch")
:disabled hidden?
:on-click on-cap-switch
:icon i/switch}]
[:& select {:default-value (:stroke-cap-end stroke)
:options stroke-caps-options
:disabled hidden?
:on-change on-caps-end-change}]])]))

View File

@ -27,6 +27,25 @@
&.dnd-over-bot {
--reorder-bottom-display: block;
}
&.hidden {
.stroke-options,
.stroke-options-tokens,
.stroke-caps-options {
opacity: 0.5;
pointer-events: none;
}
}
}
.stroke-color-actions {
display: flex;
align-items: center;
> :first-child {
flex: 1;
min-width: 0;
}
}
.stroke-options {

View File

@ -558,45 +558,46 @@
[shape-id strokes thumbnail?]
(h/call wasm/internal-module "_clear_shape_strokes")
(keep (fn [stroke]
(let [opacity (or (:stroke-opacity stroke) 1.0)
color (:stroke-color stroke)
gradient (:stroke-color-gradient stroke)
image (:stroke-image stroke)
width (:stroke-width stroke)
align (:stroke-alignment stroke)
style (-> stroke :stroke-style sr/translate-stroke-style)
cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
offset (mem/alloc types.fills.impl/FILL-U8-SIZE)
heap (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap))]
(case align
:inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
:outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
(h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end))
(when-not (:hidden stroke)
(let [opacity (or (:stroke-opacity stroke) 1.0)
color (:stroke-color stroke)
gradient (:stroke-color-gradient stroke)
image (:stroke-image stroke)
width (:stroke-width stroke)
align (:stroke-alignment stroke)
style (-> stroke :stroke-style sr/translate-stroke-style)
cap-start (-> stroke :stroke-cap-start sr/translate-stroke-cap)
cap-end (-> stroke :stroke-cap-end sr/translate-stroke-cap)
offset (mem/alloc types.fills.impl/FILL-U8-SIZE)
heap (mem/get-heap-u8)
dview (js/DataView. (.-buffer heap))]
(case align
:inner (h/call wasm/internal-module "_add_shape_inner_stroke" width style cap-start cap-end)
:outer (h/call wasm/internal-module "_add_shape_outer_stroke" width style cap-start cap-end)
(h/call wasm/internal-module "_add_shape_center_stroke" width style cap-start cap-end))
(cond
(some? gradient)
(do
(types.fills.impl/write-gradient-fill offset dview opacity gradient)
(h/call wasm/internal-module "_add_shape_stroke_fill"))
(cond
(some? gradient)
(do
(types.fills.impl/write-gradient-fill offset dview opacity gradient)
(h/call wasm/internal-module "_add_shape_stroke_fill"))
(some? image)
(let [image-id (get image :id)
buffer (uuid/get-u32 image-id)
cached-image? (h/call wasm/internal-module "_is_image_cached"
(aget buffer 0) (aget buffer 1)
(aget buffer 2) (aget buffer 3)
thumbnail?)]
(types.fills.impl/write-image-fill offset dview opacity image)
(h/call wasm/internal-module "_add_shape_stroke_fill")
(when (== cached-image? 0)
(fetch-image shape-id image-id thumbnail?)))
(some? image)
(let [image-id (get image :id)
buffer (uuid/get-u32 image-id)
cached-image? (h/call wasm/internal-module "_is_image_cached"
(aget buffer 0) (aget buffer 1)
(aget buffer 2) (aget buffer 3)
thumbnail?)]
(types.fills.impl/write-image-fill offset dview opacity image)
(h/call wasm/internal-module "_add_shape_stroke_fill")
(when (== cached-image? 0)
(fetch-image shape-id image-id thumbnail?)))
(some? color)
(do
(types.fills.impl/write-solid-fill offset dview opacity color)
(h/call wasm/internal-module "_add_shape_stroke_fill")))))
(some? color)
(do
(types.fills.impl/write-solid-fill offset dview opacity color)
(h/call wasm/internal-module "_add_shape_stroke_fill"))))))
strokes))
(defn set-shape-svg-attrs

View File

@ -7316,6 +7316,10 @@ msgstr "Outside"
msgid "workspace.options.stroke.remove-stroke"
msgstr "Remove stroke"
#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs
msgid "workspace.options.stroke.toggle-stroke"
msgstr "Toggle stroke"
#: src/app/main/ui/workspace/sidebar/options/rows/stroke_row.cljs:137
msgid "workspace.options.stroke.solid"
msgstr "Solid"