🎉 Add new page separators feature (#8561)

* 🎉 Add new page separators feature

* 📎 Add PR feedback changes

* 🐛 Fix page sitemap icons

---------

Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
Juan de la Cruz 2026-04-20 15:33:42 +02:00 committed by GitHub
parent adea81ceee
commit 876b8d645d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 130 additions and 84 deletions

View File

@ -43,6 +43,8 @@
- Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311) - Edit ruler guide position by double-clicking the guide pill (by @eureka0928) [Github #2311](https://github.com/penpot/penpot/issues/2311)
- Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653) - Add a search bar to filter colors in the color palette toolbar (by @eureka0928) [Github #7653](https://github.com/penpot/penpot/issues/7653)
- Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027) - Allow customising the OIDC login button label (by @wdeveloper16) [Github #7027](https://github.com/penpot/penpot/issues/7027)
- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806)
### :bug: Bugs fixed ### :bug: Bugs fixed

View File

@ -328,11 +328,24 @@
(ptk/reify ::rename-page (ptk/reify ::rename-page
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [page (dsh/lookup-page state id) (let [page (dsh/lookup-page state id)
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/with-page page) (pcb/with-page page)
(pcb/mod-page page {:name name}))] (pcb/mod-page page {:name name}))
(rx/of (dch/commit-changes changes)))))) pages (-> (dsh/lookup-file-data state) :pages)
index (d/index-of pages id)
prev-id (when (and (some? index) (pos? index))
(nth pages (dec index) nil))
next-id (when (some? index)
(nth pages (inc index) nil))
fallback-page-id (or prev-id next-id)
separator? (= "---" (str/trim name))]
(rx/concat
(rx/of (dch/commit-changes changes))
(when (and separator?
(= id (:current-page-id state))
(some? fallback-page-id))
(rx/of (dcm/go-to-workspace :page-id fallback-page-id))))))))
(defn- delete-page-components (defn- delete-page-components
[changes page] [changes page]

View File

@ -19,6 +19,7 @@
[:tooltip-class {:optional true} [:maybe :string]] [:tooltip-class {:optional true} [:maybe :string]]
[:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]] [:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]]
[:icon-class {:optional true} :string] [:icon-class {:optional true} :string]
[:icon-size {:optional true} [:maybe [:enum "s" "m" "l"]]]
[:icon [:icon
[:and :string [:fn #(contains? icon-list %)]]] [:and :string [:fn #(contains? icon-list %)]]]
[:aria-label :string] [:aria-label :string]
@ -30,7 +31,7 @@
(mf/defc icon-button* (mf/defc icon-button*
{::mf/schema schema:icon-button {::mf/schema schema:icon-button
::mf/memo true} ::mf/memo true}
[{:keys [class icon icon-class variant aria-label children tooltip-placement tooltip-class type] :rest props}] [{:keys [class icon icon-class icon-size variant aria-label children tooltip-placement tooltip-class type] :rest props}]
(let [variant (let [variant
(d/nilv variant "primary") (d/nilv variant "primary")
@ -60,5 +61,5 @@
:placement tooltip-placement :placement tooltip-placement
:id tooltip-id} :id tooltip-id}
[:> :button props [:> :button props
[:> icon* {:icon-id icon :aria-hidden true :class icon-class}] [:> icon* {:icon-id icon :aria-hidden true :class icon-class :size icon-size}]
children]])) children]]))

View File

@ -322,22 +322,16 @@
(mf/defc icon* (mf/defc icon*
{::mf/schema schema:icon} {::mf/schema schema:icon}
[{:keys [icon-id size class] :rest props}] [{:keys [icon-id size class] :rest props}]
(let [props (mf/spread-props props (let [size-px (cond (= size "l") icon-size-l
{:class [class (stl/css :icon)]
:width icon-size-m
:height icon-size-m})
size-px (cond (= size "l") icon-size-l
(= size "s") icon-size-s (= size "s") icon-size-s
:else icon-size-m) :else icon-size-m)
offset (if (or (= size "s") (= size "m")) props (mf/spread-props props
(/ (- icon-size-m size-px) 2) {:class [class (stl/css :icon)]
0)] :width size-px
:height size-px})]
[:> :svg props [:> :svg props
[:use {:href (dm/str "#icon-" icon-id) [:use {:href (dm/str "#icon-" icon-id)
:width size-px :width size-px
:height size-px :height size-px}]]))
:x offset
:y offset}]]))

View File

@ -9,6 +9,7 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.page :as ctp]
[app.main.data.common :as dcm] [app.main.data.common :as dcm]
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@ -19,9 +20,8 @@
[app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.badge :refer [badge-notification]]
[app.render-wasm.api :as wasm.api] [app.render-wasm.api :as wasm.api]
[app.util.dom :as dom] [app.util.dom :as dom]
@ -50,8 +50,10 @@
each object change)" each object change)"
[page-id] [page-id]
(l/derived (fn [fdata] (l/derived (fn [fdata]
(-> (dsh/get-page fdata page-id) (let [page (dsh/get-page fdata page-id)]
(dissoc :objects))) (-> page
(assoc :empty? (ctp/is-empty? page))
(dissoc :objects))))
refs/workspace-data refs/workspace-data
=)) =))
@ -62,29 +64,32 @@
(mf/defc page-item (mf/defc page-item
{::mf/wrap-props false} {::mf/wrap-props false}
[{:keys [page index deletable? selected? editing? hovering? current-page-id]}] [{:keys [page index deletable? selected? editing? hovering? current-page-id]}]
(let [input-ref (mf/use-ref) (let [input-ref (mf/use-ref)
id (:id page) id (:id page)
delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id))) name (:name page "")
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id))) is-separator? (and (= "---" (str/trim name)) (:empty? page))
read-only? (mf/use-ctx ctx/workspace-read-only?) delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
read-only? (mf/use-ctx ctx/workspace-read-only?)
on-click on-click
(mf/use-fn (mf/use-fn
(mf/deps id current-page-id) (mf/deps id current-page-id is-separator?)
(fn [] (fn []
;; For the wasm renderer, apply a blur effect to the viewport canvas (when-not is-separator?
;; when we navigate to a different page. ;; For the wasm renderer, apply a blur effect to the viewport canvas
(if (and (features/active-feature? @st/state "render-wasm/v1") ;; when we navigate to a different page.
(not= id current-page-id)) (if (and (features/active-feature? @st/state "render-wasm/v1")
(do (not= id current-page-id))
(wasm.api/capture-canvas-pixels) (do
(wasm.api/apply-canvas-blur) (wasm.api/capture-canvas-pixels)
;; NOTE: it seems we need two RAF so the blur is actually applied and visible (wasm.api/apply-canvas-blur)
;; in the canvas :( ;; NOTE: it seems we need two RAF so the blur is actually applied and visible
(timers/raf ;; in the canvas :(
(fn [] (timers/raf
(timers/raf navigate-fn)))) (fn []
(navigate-fn)))) (timers/raf navigate-fn))))
(navigate-fn)))))
on-delete on-delete
(mf/use-fn (mf/use-fn
@ -106,11 +111,14 @@
on-blur on-blur
(mf/use-fn (mf/use-fn
(mf/deps id is-separator?)
(fn [event] (fn [event]
(let [name (str/trim (dom/get-target-val event))] (let [new-name (str/trim (dom/get-target-val event))]
(when-not (str/empty? name) (if (str/empty? new-name)
(st/emit! (dw/rename-page id name))) (when is-separator?
(st/emit! (dw/stop-rename-page-item))))) (st/emit! (dw/delete-page id)))
(st/emit! (dw/rename-page id new-name))))
(st/emit! (dw/stop-rename-page-item))))
on-key-down on-key-down
(mf/use-fn (mf/use-fn
@ -166,40 +174,49 @@
(dom/select-text! edit-input)) (dom/select-text! edit-input))
nil))) nil)))
[:li {:class (stl/css-case (let [selected? (and selected? (not is-separator?))]
:page-element true [:li {:class (stl/css-case
:selected selected? :page-element true
:dnd-over-top (= (:over dprops) :top) :separator is-separator?
:dnd-over-bot (= (:over dprops) :bot)) :selected selected?
:ref dref} :dnd-over-top (= (:over dprops) :top)
[:div {:class (stl/css-case :dnd-over-bot (= (:over dprops) :bot))
:element-list-body true :ref dref}
:hover hovering? [:div {:class (stl/css-case
:selected selected?) :element-list-body true
:data-testid (dm/str "page-" id) :separator-body is-separator?
:tab-index "0" :hover (and hovering? (not is-separator?))
:on-click on-click :selected selected?)
:on-double-click on-double-click :data-testid (dm/str "page-" id)
:on-context-menu on-context-menu} :tab-index "0"
[:div {:class (stl/css :page-icon)} :on-click on-click
deprecated-icon/document] :on-double-click on-double-click
:on-context-menu on-context-menu}
(if editing? (if (and is-separator? (not editing?))
[:* [:div {:class (stl/css :page-separator)
[:input {:class (stl/css :element-name) :data-testid "page-separator"}]
:type "text" [:*
:ref input-ref (when-not is-separator?
:on-blur on-blur [:div {:class (stl/css :page-icon)}
:on-key-down on-key-down [:> icon* {:icon-id i/document :size "s"}]])
:auto-focus true (if editing?
:default-value (:name page "")}]] [:input {:class (stl/css :element-name)
[:* :type "text"
[:span {:class (stl/css :page-name) :title (:name page) :data-testid "page-name"} :ref input-ref
(:name page)] :on-blur on-blur
[:div {:class (stl/css :page-actions)} :on-key-down on-key-down
(when (and deletable? (not read-only?)) :auto-focus true
[:button {:on-click on-delete} :default-value name}]
deprecated-icon/delete])]])]])) [:*
[:span {:class (stl/css :page-name) :title name :data-testid "page-name"}
name]
[:div {:class (stl/css :page-actions)}
(when (and deletable? (not read-only?))
[:> icon-button* {:variant "ghost"
:aria-label (tr "modals.delete-page.title")
:on-click on-delete
:icon-size "s"
:icon i/delete}])]])])]])))
;; --- Page Item Wrapper ;; --- Page Item Wrapper

View File

@ -5,6 +5,7 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
.sitemap { .sitemap {
position: relative; position: relative;
@ -99,8 +100,6 @@
svg { svg {
@extend %button-icon-small; @extend %button-icon-small;
height: deprecated.$s-12;
width: deprecated.$s-12;
color: transparent; color: transparent;
fill: none; fill: none;
stroke: var(--icon-foreground); stroke: var(--icon-foreground);
@ -109,6 +108,8 @@
.page-actions { .page-actions {
height: deprecated.$s-32; height: deprecated.$s-32;
display: flex;
align-items: center;
button { button {
@include deprecated.buttonStyle; @include deprecated.buttonStyle;
@ -121,8 +122,6 @@
svg { svg {
@extend %button-icon-small; @extend %button-icon-small;
height: deprecated.$s-12;
width: deprecated.$s-12;
color: transparent; color: transparent;
fill: none; fill: none;
stroke: var(--icon-foreground); stroke: var(--icon-foreground);
@ -253,6 +252,26 @@
} }
} }
.element-list-body.separator-body {
height: auto;
min-height: var(--sp-xxxl);
padding: 0;
}
.page-separator {
width: 100%;
height: $b-1;
margin: var(--sp-s);
background-color: var(--color-background-quaternary);
}
.page-element.separator:hover .element-list-body,
.page-element.separator.hover .element-list-body {
color: var(--layer-row-foreground-color);
background-color: transparent;
box-shadow: none;
}
.title-spacing-sitemap { .title-spacing-sitemap {
padding-inline-start: deprecated.$s-8; padding-inline-start: deprecated.$s-8;
margin-block: deprecated.$s-8 deprecated.$s-4; margin-block: deprecated.$s-8 deprecated.$s-4;