From 876b8d645db57876289bb60aee7df00e4fc7bfd0 Mon Sep 17 00:00:00 2001 From: Juan de la Cruz Date: Mon, 20 Apr 2026 15:33:42 +0200 Subject: [PATCH] :tada: Add new page separators feature (#8561) * :tada: Add new page separators feature * :paperclip: Add PR feedback changes * :bug: Fix page sitemap icons --------- Co-authored-by: Andrey Antukh --- CHANGES.md | 2 + .../src/app/main/data/workspace/pages.cljs | 23 ++- .../app/main/ui/ds/buttons/icon_button.cljs | 5 +- .../main/ui/ds/foundations/assets/icon.cljs | 18 +-- .../main/ui/workspace/sidebar/sitemap.cljs | 139 ++++++++++-------- .../main/ui/workspace/sidebar/sitemap.scss | 27 +++- 6 files changed, 130 insertions(+), 84 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b695345f6d..1e02ac6c45 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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) - 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) +- Add page separators in Workspace [Taiga #13611](https://tree.taiga.io/project/penpot/us/13611?milestone=262806) + ### :bug: Bugs fixed diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 5865cb969d..222a4a5c0e 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -328,11 +328,24 @@ (ptk/reify ::rename-page ptk/WatchEvent (watch [it state _] - (let [page (dsh/lookup-page state id) - changes (-> (pcb/empty-changes it) - (pcb/with-page page) - (pcb/mod-page page {:name name}))] - (rx/of (dch/commit-changes changes)))))) + (let [page (dsh/lookup-page state id) + changes (-> (pcb/empty-changes it) + (pcb/with-page page) + (pcb/mod-page page {:name name})) + 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 [changes page] diff --git a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs index bcfd24240e..45b0b7b1b7 100644 --- a/frontend/src/app/main/ui/ds/buttons/icon_button.cljs +++ b/frontend/src/app/main/ui/ds/buttons/icon_button.cljs @@ -19,6 +19,7 @@ [:tooltip-class {:optional true} [:maybe :string]] [:type {:optional true} [:maybe [:enum "button" "submit" "reset"]]] [:icon-class {:optional true} :string] + [:icon-size {:optional true} [:maybe [:enum "s" "m" "l"]]] [:icon [:and :string [:fn #(contains? icon-list %)]]] [:aria-label :string] @@ -30,7 +31,7 @@ (mf/defc icon-button* {::mf/schema schema:icon-button ::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 (d/nilv variant "primary") @@ -60,5 +61,5 @@ :placement tooltip-placement :id tooltip-id} [:> :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]])) diff --git a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs index 0203bf9999..f2ccc02d4a 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/icon.cljs @@ -322,22 +322,16 @@ (mf/defc icon* {::mf/schema schema:icon} [{:keys [icon-id size class] :rest props}] - (let [props (mf/spread-props props - {:class [class (stl/css :icon)] - :width icon-size-m - :height icon-size-m}) - - size-px (cond (= size "l") icon-size-l + (let [size-px (cond (= size "l") icon-size-l (= size "s") icon-size-s :else icon-size-m) - offset (if (or (= size "s") (= size "m")) - (/ (- icon-size-m size-px) 2) - 0)] + props (mf/spread-props props + {:class [class (stl/css :icon)] + :width size-px + :height size-px})] [:> :svg props [:use {:href (dm/str "#icon-" icon-id) :width size-px - :height size-px - :x offset - :y offset}]])) + :height size-px}]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 9c62ed393f..3c683a80ef 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.types.page :as ctp] [app.main.data.common :as dcm] [app.main.data.helpers :as dsh] [app.main.data.modal :as modal] @@ -19,9 +20,8 @@ [app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.context :as ctx] [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.icons :as deprecated-icon] [app.main.ui.notifications.badge :refer [badge-notification]] [app.render-wasm.api :as wasm.api] [app.util.dom :as dom] @@ -50,8 +50,10 @@ each object change)" [page-id] (l/derived (fn [fdata] - (-> (dsh/get-page fdata page-id) - (dissoc :objects))) + (let [page (dsh/get-page fdata page-id)] + (-> page + (assoc :empty? (ctp/is-empty? page)) + (dissoc :objects)))) refs/workspace-data =)) @@ -62,29 +64,32 @@ (mf/defc page-item {::mf/wrap-props false} [{:keys [page index deletable? selected? editing? hovering? current-page-id]}] - (let [input-ref (mf/use-ref) - id (:id page) - 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?) + (let [input-ref (mf/use-ref) + id (:id page) + name (:name page "") + is-separator? (and (= "---" (str/trim name)) (:empty? page)) + 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 (mf/use-fn - (mf/deps id current-page-id) + (mf/deps id current-page-id is-separator?) (fn [] - ;; For the wasm renderer, apply a blur effect to the viewport canvas - ;; when we navigate to a different page. - (if (and (features/active-feature? @st/state "render-wasm/v1") - (not= id current-page-id)) - (do - (wasm.api/capture-canvas-pixels) - (wasm.api/apply-canvas-blur) - ;; NOTE: it seems we need two RAF so the blur is actually applied and visible - ;; in the canvas :( - (timers/raf - (fn [] - (timers/raf navigate-fn)))) - (navigate-fn)))) + (when-not is-separator? + ;; For the wasm renderer, apply a blur effect to the viewport canvas + ;; when we navigate to a different page. + (if (and (features/active-feature? @st/state "render-wasm/v1") + (not= id current-page-id)) + (do + (wasm.api/capture-canvas-pixels) + (wasm.api/apply-canvas-blur) + ;; NOTE: it seems we need two RAF so the blur is actually applied and visible + ;; in the canvas :( + (timers/raf + (fn [] + (timers/raf navigate-fn)))) + (navigate-fn))))) on-delete (mf/use-fn @@ -106,11 +111,14 @@ on-blur (mf/use-fn + (mf/deps id is-separator?) (fn [event] - (let [name (str/trim (dom/get-target-val event))] - (when-not (str/empty? name) - (st/emit! (dw/rename-page id name))) - (st/emit! (dw/stop-rename-page-item))))) + (let [new-name (str/trim (dom/get-target-val event))] + (if (str/empty? new-name) + (when is-separator? + (st/emit! (dw/delete-page id))) + (st/emit! (dw/rename-page id new-name)))) + (st/emit! (dw/stop-rename-page-item)))) on-key-down (mf/use-fn @@ -166,40 +174,49 @@ (dom/select-text! edit-input)) nil))) - [:li {:class (stl/css-case - :page-element true - :selected selected? - :dnd-over-top (= (:over dprops) :top) - :dnd-over-bot (= (:over dprops) :bot)) - :ref dref} - [:div {:class (stl/css-case - :element-list-body true - :hover hovering? - :selected selected?) - :data-testid (dm/str "page-" id) - :tab-index "0" - :on-click on-click - :on-double-click on-double-click - :on-context-menu on-context-menu} - [:div {:class (stl/css :page-icon)} - deprecated-icon/document] - - (if editing? - [:* - [:input {:class (stl/css :element-name) - :type "text" - :ref input-ref - :on-blur on-blur - :on-key-down on-key-down - :auto-focus true - :default-value (:name page "")}]] - [:* - [:span {:class (stl/css :page-name) :title (:name page) :data-testid "page-name"} - (:name page)] - [:div {:class (stl/css :page-actions)} - (when (and deletable? (not read-only?)) - [:button {:on-click on-delete} - deprecated-icon/delete])]])]])) + (let [selected? (and selected? (not is-separator?))] + [:li {:class (stl/css-case + :page-element true + :separator is-separator? + :selected selected? + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :ref dref} + [:div {:class (stl/css-case + :element-list-body true + :separator-body is-separator? + :hover (and hovering? (not is-separator?)) + :selected selected?) + :data-testid (dm/str "page-" id) + :tab-index "0" + :on-click on-click + :on-double-click on-double-click + :on-context-menu on-context-menu} + (if (and is-separator? (not editing?)) + [:div {:class (stl/css :page-separator) + :data-testid "page-separator"}] + [:* + (when-not is-separator? + [:div {:class (stl/css :page-icon)} + [:> icon* {:icon-id i/document :size "s"}]]) + (if editing? + [:input {:class (stl/css :element-name) + :type "text" + :ref input-ref + :on-blur on-blur + :on-key-down on-key-down + :auto-focus true + :default-value name}] + [:* + [: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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index 51b5f9fc5e..3a7a3233ac 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -5,6 +5,7 @@ // Copyright (c) KALEIDOS INC @use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; .sitemap { position: relative; @@ -99,8 +100,6 @@ svg { @extend %button-icon-small; - height: deprecated.$s-12; - width: deprecated.$s-12; color: transparent; fill: none; stroke: var(--icon-foreground); @@ -109,6 +108,8 @@ .page-actions { height: deprecated.$s-32; + display: flex; + align-items: center; button { @include deprecated.buttonStyle; @@ -121,8 +122,6 @@ svg { @extend %button-icon-small; - height: deprecated.$s-12; - width: deprecated.$s-12; color: transparent; fill: none; 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 { padding-inline-start: deprecated.$s-8; margin-block: deprecated.$s-8 deprecated.$s-4;