From 5dff551f3128f71f6e4318d2945ee7678a60d992 Mon Sep 17 00:00:00 2001 From: Juan de la Cruz Date: Tue, 21 Jul 2026 14:26:03 +0200 Subject: [PATCH] :tada: Add page multi-selection in the workspace sitemap (#10581) * :tada: Add page multi-selection in the workspace sitemap * :recycle: Simplify page selection state updates with single assoc * :bug: Fix SCSS issue * :recycle: Update some components to new syntax * :recycle: Adapt SCSS to the new guidelines * :recycle: SCSS cleanup --------- Co-authored-by: Luis de Dios --- frontend/src/app/main/data/workspace.cljs | 22 +- .../src/app/main/data/workspace/pages.cljs | 104 ++++++- frontend/src/app/main/refs.cljs | 4 + .../app/main/ui/workspace/context_menu.cljs | 29 +- .../main/ui/workspace/sidebar/sitemap.cljs | 187 ++++++------ .../main/ui/workspace/sidebar/sitemap.scss | 273 ++++++------------ frontend/translations/en.po | 12 + frontend/translations/es.po | 12 + 8 files changed, 372 insertions(+), 271 deletions(-) diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 6ed0feec97..fc2404f39b 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -1207,9 +1207,20 @@ (dm/assert! (gpt/point? position)) (ptk/reify ::show-page-item-context-menu ptk/WatchEvent - (watch [_ _ _] - (rx/of (show-context-menu - (-> params (assoc :kind :page :selected (:id page)))))))) + (watch [_ state _] + (let [id (:id page) + selected (dm/get-in state [:workspace-local :selected-pages]) + ;; When the right-clicked page is part of a multi-selection we + ;; keep it; otherwise the menu targets just that page. + multi? (and (contains? selected id) (> (count selected) 1))] + (rx/concat + (if multi? + (rx/empty) + (rx/of (dwpg/select-page id))) + (rx/of (show-context-menu + (-> params (assoc :kind :page + :selected id + :selected-pages (if multi? selected #{id})))))))))) (defn show-track-context-menu [{:keys [grid-id type index] :as params}] @@ -1651,6 +1662,11 @@ (dm/export dwpg/duplicate-page) (dm/export dwpg/rename-page) (dm/export dwpg/delete-page) +(dm/export dwpg/delete-pages) +(dm/export dwpg/select-page) +(dm/export dwpg/toggle-page-selection) +(dm/export dwpg/select-pages-range) +(dm/export dwpg/clear-page-selection) ;; Shapes (dm/export dwsh/delete-shapes) diff --git a/frontend/src/app/main/data/workspace/pages.cljs b/frontend/src/app/main/data/workspace/pages.cljs index 37d12730b8..0bfa559469 100644 --- a/frontend/src/app/main/data/workspace/pages.cljs +++ b/frontend/src/app/main/data/workspace/pages.cljs @@ -135,7 +135,8 @@ ptk/UpdateEvent (update [_ state] (let [local (-> (:workspace-local state) - (dissoc :edition :edit-path :selected)) + (dissoc :edition :edit-path :selected + :selected-pages :selected-pages-anchor)) exit? (not= :workspace (rt/lookup-name state)) state (-> state (update :workspace-cache assoc [file-id page-id] local) @@ -395,3 +396,104 @@ (rx/of (dch/commit-changes changes) (when (= id (:current-page-id state)) (dcm/go-to-workspace {:page-id (first pages)}))))))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Page selection (sitemap multi-selection) +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn clear-page-selection + "Clear the sitemap multi-selection state." + [] + (ptk/reify ::clear-page-selection + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local dissoc :selected-pages :selected-pages-anchor)))) + +(defn select-page + "Set the sitemap selection to a single page (plain click). It also + sets the anchor used by range (shift+click) selection." + [id] + (ptk/reify ::select-page + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local assoc + :selected-pages (d/ordered-set id) + :selected-pages-anchor id)))) + +(defn toggle-page-selection + "Add or remove a page from the sitemap multi-selection (ctrl/cmd + click)." + [id] + (ptk/reify ::toggle-page-selection + ptk/UpdateEvent + (update [_ state] + (let [current-id (:current-page-id state) + selected (or (not-empty (dm/get-in state [:workspace-local :selected-pages])) + (d/ordered-set current-id)) + selected (if (contains? selected id) + (disj selected id) + (conj selected id))] + (update state :workspace-local assoc + :selected-pages selected + :selected-pages-anchor id))))) + +(defn select-pages-range + "Select every page between the current anchor and `id`, both included + (shift + click). The anchor is kept unchanged." + [id] + (ptk/reify ::select-pages-range + ptk/UpdateEvent + (update [_ state] + (let [current-id (:current-page-id state) + pages (-> (dsh/lookup-file-data state) :pages vec) + anchor (or (dm/get-in state [:workspace-local :selected-pages-anchor]) + current-id) + a-idx (d/index-of pages anchor) + b-idx (d/index-of pages id)] + (if (and (some? a-idx) (some? b-idx)) + (let [start (min a-idx b-idx) + end (inc (max a-idx b-idx)) + range (subvec pages start end)] + (update state :workspace-local assoc + :selected-pages (into (d/ordered-set) range))) + state))))) + +(defn delete-pages + "Delete a collection of pages in a single change (single undo entry), + always keeping at least one page in the file." + [ids] + (ptk/reify ::delete-pages + ptk/WatchEvent + (watch [it state _] + (let [file-id (:current-file-id state) + fdata (dsh/lookup-file-data state file-id) + pindex (:pages-index fdata) + pages (:pages fdata) + + ids (set ids) + ;; A file must keep at least one page: if every page is + ;; selected, keep the first one in page order. + ids (if (>= (count ids) (count pages)) + (set (rest (filter ids pages))) + ids) + + ;; Pages to delete, in page order. + del-ids (filter ids pages) + remaining (remove ids pages) + + changes (reduce + (fn [changes id] + (let [page (-> (get pindex id) + (assoc :index (d/index-of pages id)))] + (-> changes + (delete-page-components page) + (pcb/del-page page)))) + (-> (pcb/empty-changes it) + (pcb/with-library-data fdata)) + del-ids)] + + (if (empty? del-ids) + (rx/empty) + (rx/of (dch/commit-changes changes) + (clear-page-selection) + (when (contains? ids (:current-page-id state)) + (dcm/go-to-workspace {:page-id (first remaining)})))))))) diff --git a/frontend/src/app/main/refs.cljs b/frontend/src/app/main/refs.cljs index cf6e9e17f9..08d0a3ab7a 100644 --- a/frontend/src/app/main/refs.cljs +++ b/frontend/src/app/main/refs.cljs @@ -253,6 +253,10 @@ (def editing-page-item (l/derived :page-item workspace-local)) +;; set of pages selected in the sitemap (multi-selection) +(def selected-pages + (l/derived :selected-pages workspace-local)) + (def current-hover-ids (l/derived :hover-ids context-menu)) diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 6d1384790c..d79a194163 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -705,6 +705,8 @@ [{:keys [mdata]}] (let [page (:page mdata) deletable? (:deletable? mdata) + selected-pages (:selected-pages mdata) + multi? (> (count selected-pages) 1) id (:id page) delete-fn #(st/emit! (dw/delete-page id)) do-delete #(st/emit! (modal/show @@ -712,20 +714,31 @@ :title (tr "modals.delete-page.title") :message (tr "modals.delete-page.body") :on-accept delete-fn})) + delete-many-fn #(st/emit! (dw/delete-pages selected-pages)) + do-delete-many #(st/emit! (modal/show + {:type :confirm + :title (tr "modals.delete-pages.title") + :message (tr "modals.delete-pages.body") + :on-accept delete-many-fn})) do-duplicate #(st/emit! (dw/duplicate-page id) (ev/event {::ev/name "duplicate-page"})) do-rename #(st/emit! (dw/start-rename-page-item id))] - [:* - (when deletable? - [:> menu-entry* {:title (tr "workspace.assets.delete") - :on-click do-delete}]) + (if multi? + ;; When several pages are selected, the only available action is + ;; deleting all of them at once. + [:> menu-entry* {:title (tr "workspace.assets.delete-pages") + :on-click do-delete-many}] + [:* + (when deletable? + [:> menu-entry* {:title (tr "workspace.assets.delete") + :on-click do-delete}]) - [:> menu-entry* {:title (tr "workspace.assets.rename") - :on-click do-rename}] - [:> menu-entry* {:title (tr "workspace.assets.duplicate") - :on-click do-duplicate}]])) + [:> menu-entry* {:title (tr "workspace.assets.rename") + :on-click do-rename}] + [:> menu-entry* {:title (tr "workspace.assets.duplicate") + :on-click do-duplicate}]]))) (mf/defc viewport-context-menu* [] diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 3283050d23..bcb0771477 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -62,9 +62,10 @@ ;; --- Page Item -(mf/defc page-item - {::mf/wrap-props false} - [{:keys [page index deletable? selected? editing? hovering? current-page-id]}] +(mf/defc page-item* + {::mf/private true + ::mf/wrap-props false} + [{:keys [page index is-deletable is-selected is-editing is-hovering current-page-id]}] (let [input-ref (mf/use-ref) id (:id page) name (:name page "") @@ -76,38 +77,53 @@ on-click (mf/use-fn (mf/deps id current-page-id is-separator?) - (fn [] + (fn [event] (when-not is-separator? - ;; WASM page transitions: - ;; - Capture the current page (A) once - ;; - Show a blurred snapshot while the target page (B/C/...) renders - ;; - If the user clicks again during the transition, keep showing the original (A) snapshot - (if (and (features/active-feature? @st/state "render-wasm/v1") - (not= id current-page-id)) - (-> (if @wasm.api/page-transition? - (p/resolved nil) - ;; Blur with Skia, then capture the already-blurred frame. - (do (wasm.api/render-blurred-snapshot!) - (wasm.api/capture-canvas-snapshot))) - (p/finally - (fn [] - (wasm.api/apply-canvas-blur) - ;; Two RAF so the overlay paints before navigation. - (timers/raf - (fn [] - (timers/raf navigate-fn)))))) - (navigate-fn))))) + (cond + ;; Shift + click: select the range of pages from the anchor + ;; to this page (does not navigate). + (kbd/shift? event) + (st/emit! (dw/select-pages-range id)) + + ;; Ctrl/Cmd + click: add/remove this page to/from the + ;; multi-selection (does not navigate). + (kbd/mod? event) + (st/emit! (dw/toggle-page-selection id)) + + ;; Plain click: reset the selection to this page and + ;; navigate to it. + :else + (do + (st/emit! (dw/select-page id)) + ;; WASM page transitions: + ;; - Capture the current page (A) once + ;; - Show a blurred snapshot while the target page (B/C/...) renders + ;; - If the user clicks again during the transition, keep showing the original (A) snapshot + (if (and (features/active-feature? @st/state "render-wasm/v1") + (not= id current-page-id)) + (-> (if @wasm.api/page-transition? + (p/resolved nil) + ;; Blur with Skia, then capture the already-blurred frame. + (do (wasm.api/render-blurred-snapshot!) + (wasm.api/capture-canvas-snapshot))) + (p/finally + (fn [] + (wasm.api/apply-canvas-blur) + ;; Two RAF so the overlay paints before navigation. + (timers/raf + (fn [] + (timers/raf navigate-fn)))))) + (navigate-fn))))))) on-delete (mf/use-fn (mf/deps id) (fn [event] (dom/stop-propagation event) - (st/emit! (modal/show - {:type :confirm - :title (tr "modals.delete-page.title") - :message (tr "modals.delete-page.body") - :on-accept delete-fn})))) + (st/emit! (modal/show {:type :confirm + :title (tr "modals.delete-page.title") + :message (tr "modals.delete-page.body") + :on-accept delete-fn})))) on-double-click (mf/use-fn @@ -153,7 +169,7 @@ :data {:id id :index index :name (:name page)} - :draggable? (and (not read-only?) (not editing?))) + :draggable? (and (not read-only?) (not is-editing))) on-context-menu (mf/use-fn @@ -166,50 +182,49 @@ (st/emit! (dw/show-page-item-context-menu {:position position :page page - :deletable? deletable?}))))))] + :deletable? is-deletable}))))))] (mf/use-effect - (mf/deps selected?) + (mf/deps is-selected) (fn [] - (when selected? + (when is-selected (let [node (mf/ref-val dref)] (dom/scroll-into-view-if-needed! node))))) (mf/use-layout-effect - (mf/deps editing?) + (mf/deps is-editing) (fn [] - (when editing? + (when is-editing (let [edit-input (mf/ref-val input-ref)] (dom/select-text! edit-input)) nil))) - (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)) + (let [selected? (and is-selected (not is-separator?))] + [:li {:class (stl/css-case :page-item 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?) + [:div {:class (stl/css-case :page-item-body true + :separator is-separator? + :hover (and is-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) + (if (and is-separator? (not is-editing)) + [:div {:class (stl/css :page-divider) :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) + [:div {:class (stl/css :page-item-icon)} + [:> icon* {:icon-id i/document + :size "s"}]]) + (if is-editing + [:input {:class (stl/css :page-item-input) :type "text" :ref input-ref :on-blur on-blur @@ -217,32 +232,35 @@ :auto-focus true :default-value name}] [:* - [:span {:class (stl/css :page-name) :title name :data-testid "page-name"} + [:span {:class (stl/css :page-item-label) + :title name + :data-testid "page-name"} name] - [:div {:class (stl/css :page-actions)} - (when (and deletable? (not read-only?)) + [:div {:class (stl/css :page-item-actions)} + (when (and is-deletable (not read-only?)) [:> icon-button* {:variant "action" :aria-label (tr "modals.delete-page.title") :on-click on-delete :icon-size "s" - :class (stl/css :page-delete-button) - :icon-class (stl/css :page-delete-button-icon) + :class (stl/css :page-delete-btn) + :icon-class (stl/css :page-delete-icon) :icon i/delete}])]])])]]))) ;; --- Page Item Wrapper -(mf/defc page-item-wrapper - {::mf/wrap-props false} - [{:keys [page-id index deletable? selected? editing? current-page-id]}] +(mf/defc page-item-wrapper* + {::mf/private true + ::mf/wrap-props false} + [{:keys [page-id index is-deletable is-selected is-editing current-page-id]}] (let [page-ref (mf/with-memo [page-id] (make-page-ref page-id)) page (mf/deref page-ref)] - [:& page-item {:page page - :index index - :current-page-id current-page-id - :deletable? deletable? - :selected? selected? - :editing? editing?}])) + [:> page-item* {:page page + :index index + :current-page-id current-page-id + :is-deletable is-deletable + :is-selected is-selected + :is-editing is-editing}])) ;; --- Pages List @@ -252,18 +270,23 @@ (let [pages (:pages file) deletable? (> (count pages) 1) editing-page-id (mf/deref refs/editing-page-item) - current-page-id (mf/use-ctx ctx/current-page-id)] - [:ul {:class (stl/css :page-list)} + selected-pages (mf/deref refs/selected-pages) + current-page-id (mf/use-ctx ctx/current-page-id) + ;; When there is no explicit multi-selection, the current page + ;; is the selected one. + selected-pages (if (seq selected-pages) + selected-pages + #{current-page-id})] + [:ul [:> hooks/sortable-container* {} (for [[index page-id] (d/enumerate pages)] - [:& page-item-wrapper - {:page-id page-id - :index index - :deletable? deletable? - :editing? (= page-id editing-page-id) - :selected? (= page-id current-page-id) - :current-page-id current-page-id - :key page-id}])]])) + [:> page-item-wrapper* {:page-id page-id + :index index + :is-deletable deletable? + :is-editing (= page-id editing-page-id) + :is-selected (contains? selected-pages page-id) + :current-page-id current-page-id + :key page-id}])]])) ;; --- Sitemap Toolbox @@ -276,7 +299,8 @@ on-create (mf/use-fn (mf/deps file-id project-id) (fn [event] - (st/emit! (dw/create-page {:file-id file-id :project-id project-id})) + (st/emit! (dw/create-page {:file-id file-id + :project-id project-id})) (-> event dom/get-current-target dom/blur!))) read-only? (mf/use-ctx ctx/workspace-read-only?) @@ -289,7 +313,7 @@ :collapsed collapsed :on-collapsed on-toggle-collapsed :title (tr "workspace.sidebar.sitemap") - :class (stl/css :title-spacing-sitemap)} + :class (stl/css :sitemap-title)} (if ^boolean read-only? (when ^boolean (:can-edit permissions) @@ -297,12 +321,11 @@ :size :small :content (tr "labels.view-only")}]) [:> icon-button* {:variant "ghost" - :class (stl/css :add-page) :aria-label (tr "workspace.sidebar.sitemap.add-page") :on-click on-create :icon i/add}])] (when-not ^boolean collapsed - [:div {:class (stl/css :tool-window-content)} - [:> pages-list* {:file file :key (dm/str (:id file))}]])])) - + [:div {:class (stl/css :sitemap-content)} + [:> pages-list* {:key (dm/str (:id file)) + :file file}]])])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss index fb5e59cde6..4bfbe1c95d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.scss @@ -4,236 +4,155 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_utils.scss" as *; @use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/mixins.scss" as *; .sitemap { position: relative; display: flex; flex-direction: column; flex: 1; - width: 100%; - height: var(--height, deprecated.$s-200); + inline-size: 100%; + block-size: var(--height, $sz-200); } -.title { - margin-left: deprecated.$s-2; - color: var(--title-foreground-color-hover); +.sitemap-title { + padding-inline-start: var(--sp-s); + margin-block: var(--sp-s) var(--sp-xs); } -.resize-area { - position: absolute; - bottom: calc(-1 * deprecated.$s-8); - left: 0; - width: 100%; - height: deprecated.$s-12; - border-top: deprecated.$s-2 solid var(--resize-area-border-color); - background-color: var(--resize-area-background-color); - cursor: ns-resize; - - &:hover { - border-color: var(--resize-area-border-color); - } -} - -.tool-window-content { +.sitemap-content { display: flex; flex-direction: column; - height: calc(-38px + var(--height, deprecated.$s-200)); - width: var(--left-sidebar-width); + inline-size: var(--left-sidebar-width); overflow: hidden auto; scrollbar-gutter: stable; - - .element-list { - display: grid; - } } -.pages-list { - width: 100%; - max-height: deprecated.$s-152; - margin-bottom: deprecated.$s-12; -} +.page-item { + @include t.use-typography("body-small"); -.page-delete-button { - --delete-button-display: none; -} - -.page-delete-button-icon { - display: var(--delete-button-display); -} - -.page-element { - @include deprecated.body-small-typography; - - min-height: deprecated.$s-32; - width: 100%; - cursor: pointer; + min-block-size: $sz-32; + inline-size: 100%; &.dnd-over-top { - border-top: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); + border-block-start: $b-1 solid var(--layer-row-foreground-color-drag); } &.dnd-over-bot { - border-bottom: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); + border-block-end: $b-1 solid var(--layer-row-foreground-color-drag); } - .dnd-over > .element-list-body { - border: deprecated.$s-1 solid var(--layer-row-foreground-color-drag); - } - - .element-list-body { + .page-item-body { display: flex; align-items: center; - height: deprecated.$s-32; - width: 100%; - padding: 0 deprecated.$s-12 0 0; + block-size: $sz-32; + inline-size: 100%; + padding: 0 var(--sp-m) 0 0; transition: none; color: var(--layer-row-foreground-color); + user-select: none; - .page-name { - @include deprecated.text-ellipsis; - - flex-grow: 1; - padding-left: deprecated.$s-2; - } - - .page-icon { - @include deprecated.flex-center; - - padding: 0 deprecated.$s-4 0 deprecated.$s-8; - } - - .page-actions { - height: deprecated.$s-32; - display: flex; - align-items: center; - } - - .element-name { - @include deprecated.text-ellipsis; - - color: var(--layer-row-foreground-color-focus); - } - - input.element-name { - @include deprecated.text-ellipsis; - @include deprecated.body-small-typography; - @include deprecated.remove-input-style; - - flex-grow: 1; - height: deprecated.$s-28; - max-width: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); - padding-left: deprecated.$s-6; - margin: 0; - border-radius: deprecated.$br-8; - border: deprecated.$s-1 solid var(--input-border-color-focus); - color: var(--layer-row-foreground-color); + &.separator { + block-size: auto; + min-block-size: var(--sp-xxxl); + padding: 0; } } - &:active, - &.on-drag { - .element-list-body { - color: var(--layer-row-foreground-color-drag); - background-color: var(--layer-row-background-color-drag); + .page-item-label { + @include text-ellipsis; - .page-icon svg { - stroke: var(--layer-row-foreground-color-drag); - } - } + flex-grow: 1; + padding-inline-start: var(--sp-xxs); } - &.selected, - &.selected:hover { - .element-list-body { - color: var(--layer-row-foreground-color-selected); - background-color: var(--layer-row-background-color-selected); - box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0 - var(--layer-row-background-color-selected); - - .page-icon svg { - stroke: var(--layer-row-foreground-color-selected); - } - } + .page-item-icon { + display: flex; + justify-content: center; + align-items: center; + padding: 0 var(--sp-xs) 0 var(--sp-s); } - &:hover, - &.hover { - .element-list-body { - color: var(--layer-row-foreground-color-hover); - background-color: var(--layer-row-background-color-hover); - box-shadow: deprecated.$s-16 deprecated.$s-0 deprecated.$s-0 deprecated.$s-0 - var(--layer-row-background-color-hover); - - .page-actions button { - opacity: deprecated.$op-10; - - --delete-button-display: initial; - } - - .page-icon svg { - stroke: var(--layer-row-foreground-color-hover); - } - } + .page-item-actions { + block-size: $sz-32; + display: flex; + align-items: center; } - &:focus { - .element-list-body { - color: var(--layer-row-foreground-color-focus); - border: deprecated.$s-1 solid var(--layer-row-border-color-focus); - outline: none; + .page-item-input { + @include text-ellipsis; + @include t.use-typography("body-small"); - .page-actions button { - opacity: deprecated.$op-10; - } - } + background: none; + outline: none; + flex-grow: 1; + block-size: $sz-28; + max-inline-size: calc(var(--parent-size) - (var(--depth) * var(--layer-indentation-size))); + padding-inline-start: $sz-6; + margin: 0; + border-radius: $br-8; + border: $b-1 solid var(--input-border-color-focus); + color: var(--layer-row-foreground-color); } - &:focus-within { - .element-list-body { - outline: none; - - .page-actions button { - opacity: deprecated.$op-10; - } - } + &:active .page-item-body { + color: var(--layer-row-foreground-color-drag); + background-color: var(--layer-row-background-color-drag); } - &.hidden { - .element-list-body { - color: var(--layer-row-foreground-color-hidden); - background-color: var(--layer-row-background-color-hidden); - opacity: deprecated.$op-7; + &.selected .page-item-body, + &.selected:hover .page-item-body { + color: var(--layer-row-foreground-color-selected); + background-color: var(--layer-row-background-color-selected); + box-shadow: $sz-16 0 0 0 var(--layer-row-background-color-selected); + } - .page-icon svg { - stroke: var(--layer-row-foreground-color-hidden); - } - } + &:hover .page-item-body, + &.hover .page-item-body { + color: var(--layer-row-foreground-color-hover); + background-color: var(--layer-row-background-color-hover); + box-shadow: $sz-16 0 0 0 var(--layer-row-background-color-hover); + } + + &:hover .page-delete-btn, + &.hover .page-delete-btn { + --delete-button-display: initial; + } + + &:focus .page-item-body { + color: var(--layer-row-foreground-color-focus); + border: $b-1 solid var(--layer-row-border-color-focus); + outline: none; + } + + &:focus-within .page-item-body { + outline: none; + } + + &.separator:hover .page-item-body, + &.separator.hover .page-item-body { + color: var(--layer-row-foreground-color); + background-color: transparent; + box-shadow: none; } } -.element-list-body.separator-body { - height: auto; - min-height: var(--sp-xxxl); - padding: 0; -} - -.page-separator { - width: 100%; - height: $b-1; +.page-divider { + inline-size: 100%; + block-size: $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; +.page-delete-btn { + --delete-button-display: none; } -.title-spacing-sitemap { - padding-inline-start: deprecated.$s-8; - margin-block: deprecated.$s-8 deprecated.$s-4; +.page-delete-icon { + display: var(--delete-button-display); } diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 19767f7192..d54c01b33e 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -3926,6 +3926,14 @@ msgstr "Are you sure you want to delete this page?" msgid "modals.delete-page.title" msgstr "Delete page" +#: src/app/main/ui/workspace/context_menu.cljs:712 +msgid "modals.delete-pages.body" +msgstr "Are you sure you want to delete the selected pages?" + +#: src/app/main/ui/workspace/context_menu.cljs:711 +msgid "modals.delete-pages.title" +msgstr "Delete pages" + #: src/app/main/ui/dashboard/project_menu.cljs:73 msgid "modals.delete-project-confirm.accept" msgstr "Delete project" @@ -6441,6 +6449,10 @@ msgstr "Your items are going to be named automatically as \"group name / item na msgid "workspace.assets.delete" msgstr "Delete" +#: src/app/main/ui/workspace/context_menu.cljs:733 +msgid "workspace.assets.delete-pages" +msgstr "Delete pages" + #: src/app/main/ui/workspace/sidebar/assets/groups.cljs:74 msgid "workspace.assets.delete-group" msgstr "Delete group" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9bfa667435..581aeb3551 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -3810,6 +3810,14 @@ msgstr "¿Seguro que quieres borrar esta página?" msgid "modals.delete-page.title" msgstr "Borrar página" +#: src/app/main/ui/workspace/context_menu.cljs:712 +msgid "modals.delete-pages.body" +msgstr "¿Seguro que quieres borrar las páginas seleccionadas?" + +#: src/app/main/ui/workspace/context_menu.cljs:711 +msgid "modals.delete-pages.title" +msgstr "Borrar páginas" + #: src/app/main/ui/dashboard/project_menu.cljs:73 msgid "modals.delete-project-confirm.accept" msgstr "Eliminar proyecto" @@ -6317,6 +6325,10 @@ msgstr "" msgid "workspace.assets.delete" msgstr "Borrar" +#: src/app/main/ui/workspace/context_menu.cljs:733 +msgid "workspace.assets.delete-pages" +msgstr "Borrar páginas" + #: src/app/main/ui/workspace/context_menu.cljs:721, src/app/main/ui/workspace/sidebar/assets/colors.cljs:262, src/app/main/ui/workspace/sidebar/assets/typographies.cljs:471, src/app/main/ui/workspace/sidebar/options/menus/typography.cljs:537 msgid "workspace.assets.duplicate" msgstr "Duplicar"