diff --git a/frontend/src/app/main/ui/dashboard/deleted.cljs b/frontend/src/app/main/ui/dashboard/deleted.cljs index e5be919ea3..9a46b697ba 100644 --- a/frontend/src/app/main/ui/dashboard/deleted.cljs +++ b/frontend/src/app/main/ui/dashboard/deleted.cljs @@ -17,6 +17,7 @@ [app.main.store :as st] [app.main.ui.components.context-menu-a11y :refer [context-menu*]] [app.main.ui.dashboard.grid :refer [grid*]] + [app.main.ui.dashboard.layout-toggle :as lt :refer [layout-toggle*]] [app.main.ui.dashboard.subscription :refer [get-subscription-type]] [app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] @@ -57,10 +58,13 @@ (mf/defc header* {::mf/private true} - [] - [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} + [{:keys [layout on-change]}] + [:header {:class (stl/css :dashboard-header) + :data-testid "dashboard-header"} [:div#dashboard-deleted-title {:class (stl/css :dashboard-title)} - [:h1 (tr "dashboard.projects-title")]]]) + [:h1 (tr "dashboard.projects-title")]] + [:div {:class (stl/css :dashboard-header-actions)} + [:> layout-toggle* {:layout layout :on-change on-change}]]]) (mf/defc project-context-menu* {::mf/private true} @@ -87,18 +91,17 @@ :id "project-delete" :handler on-delete-project}])] - [:> context-menu* - {:on-close on-close - :show show - :fixed (or (not= top 0) (not= left 0)) - :min-width true - :top top - :left left - :options options}])) + [:> context-menu* {:on-close on-close + :show show + :fixed (or (not= top 0) (not= left 0)) + :min-width true + :top top + :left left + :options options}])) (mf/defc deleted-project-item* {::mf/private true} - [{:keys [project files]}] + [{:keys [project files layout]}] (let [project-files (filterv #(= (:project-id %) (:id project)) files) empty? (empty? project-files) @@ -176,14 +179,14 @@ :type 1 :subtitle (tr "dashboard.empty-placeholder-files-subtitle")}] - [:> grid* - {:project project - :files project-files - :origin :deleted - :can-edit false - :can-restore true - :limit limit - :selected-files selected-files}])]])) + [:> grid* {:project project + :files project-files + :origin :deleted + :can-edit false + :can-restore true + :limit limit + :layout layout + :selected-files selected-files}])]])) (mf/defc menu* {::mf/private true} @@ -217,7 +220,15 @@ (mf/defc deleted-section* [{:keys [team projects]}] - (let [deleted-map + (let [layout* (hooks/use-persisted-state lt/layout-key lt/default-layout) + layout (deref layout*) + + on-layout-change + (mf/use-fn + (fn [value] + (reset! layout* (keyword value)))) + + deleted-map (mf/deref ref:deleted-files) projects @@ -241,7 +252,7 @@ ;; Calculate deletion days based on team subscription deletion-days - (let [profile (mf/deref refs/profile) + (let [profile (mf/deref refs/profile) subscription-type (get-subscription-type (:subscription team)) nitrate-type (get-in profile [:subscription :type]) nitrate-active? (dnt/is-valid-license? profile)] @@ -286,13 +297,16 @@ (dd/clear-selected-files))) [:* - [:> header* {:team team}] + [:> header* {:team team + :layout layout + :on-change on-layout-change}] [:section {:class (stl/css :dashboard-container :no-bg) :data-testid "deleted-page-section"} [:* [:div {:class (stl/css :no-bg)} - [:> menu* {:team-id team-id :section :dashboard-deleted}] + [:> menu* {:team-id team-id + :section :dashboard-deleted}] (if (seq projects) [:* @@ -322,6 +336,7 @@ (sort-by :modified-at #(compare %2 %1))))] [:> deleted-project-item* {:project project :files files + :layout layout :key id}]))] ;; when no deleted projects diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index e602720c72..05ed77851e 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -16,6 +16,7 @@ [app.main.store :as st] [app.main.ui.dashboard.grid :refer [grid*]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.layout-toggle :as lt :refer [layout-toggle*]] [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]] @@ -32,7 +33,7 @@ (mf/defc header* {::mf/private true} - [{:keys [project create-fn can-edit]}] + [{:keys [project create-fn can-edit layout on-change]}] (let [project-id (:id project) local @@ -73,7 +74,8 @@ (dd/clear-selected-files))))] - [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} + [:header {:class (stl/css :dashboard-header) + :data-testid "dashboard-header"} (if (:is-default project) [:div#dashboard-drafts-title {:class (stl/css :dashboard-title)} [:h1 (tr "labels.drafts")]] @@ -95,6 +97,9 @@ (:name project)]])) [:div {:class (stl/css :dashboard-header-actions)} + [:> layout-toggle* {:layout layout + :on-change on-change}] + (when ^boolean can-edit [:a {:class (stl/css :btn-secondary :btn-small :new-file) :tab-index "0" @@ -155,6 +160,14 @@ selected-files (mf/deref refs/selected-files) + layout* (hooks/use-persisted-state lt/layout-key lt/default-layout) + layout (deref layout*) + + on-layout-change + (mf/use-fn + (fn [value] + (reset! layout* (keyword value)))) + on-file-created (mf/use-fn (fn [file-data] @@ -188,7 +201,9 @@ [:> header* {:team team :can-edit can-edit? :project project - :create-fn create-file}] + :create-fn create-file + :layout layout + :on-change on-layout-change}] [:section {:class (stl/css :dashboard-container :no-bg) :ref rowref} (if empty-state-viewer @@ -206,5 +221,5 @@ :can-edit can-edit? :origin :files :create-fn create-file - :limit limit}])]])) - + :limit limit + :layout layout}])]])) diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index eeec8323d1..0b19614858 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -31,9 +31,9 @@ [app.main.ui.dashboard.import :refer [use-import-file]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] [app.main.ui.dashboard.placeholder :refer [empty-grid-placeholder* loading-placeholder*]] + [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.hooks :as h] - [app.main.ui.icons :as deprecated-icon] [app.main.worker :as mw] [app.util.color :as uc] [app.util.dom :as dom] @@ -108,8 +108,8 @@ :message (ex-message cause)))))] (partial rx/dispose! subscription)))) - [:div {:class (stl/css-case :grid-item-th true - :deleted-item can-restore) + [:div {:class (stl/css-case :grid-item-thumbnail true + :is-deleted can-restore) :style {:background-color bg-color} :ref container} (when visible? @@ -127,9 +127,6 @@ ;; --- Grid Item Library -(def ^:private menu-icon - (deprecated-icon/icon-xref :menu (stl/css :menu-icon))) - (mf/defc grid-item-library* [{:keys [file can-restore]}] (mf/with-effect [file] @@ -137,113 +134,115 @@ (let [font-ids (map :font-id (get-in file [:library-summary :typographies :sample] []))] (run! fonts/ensure-loaded! font-ids)))) - [:div {:class (stl/css-case :grid-item-th true - :library true - :deleted-item can-restore)} + [:div {:class (stl/css-case :library-thumbnail true + :is-deleted can-restore)} (if (nil? file) [:> loader* {:class (stl/css :grid-loader) :overlay true :title (tr "labels.loading")}] - (let [summary (:library-summary file) - components (:components summary) - colors (:colors summary) + (let [summary (:library-summary file) + components (:components summary) + colors (:colors summary) typographies (:typographies summary)] [:* (when (and (zero? (:count components)) (zero? (:count colors)) (zero? (:count typographies))) [:* - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.components")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.colors")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") 0 ")"]]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.typography")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") 0 ")"]]]]) ;; Unicode 00A0 is non-breaking space + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") 0 ")"]]]]) ;; Unicode 00A0 is non-breaking space (when (pos? (:count components)) - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.components")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count components) ")"]] ;; Unicode 00A0 is non-breaking space - [:div {:class (stl/css :asset-list)} + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") (:count components) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :library-asset-list)} (for [component (:sample components)] (let [root-id (:main-instance-id component)] - [:div {:class (stl/css :asset-list-item) + [:div {:class (stl/css :library-asset-item) :key (str "assets-component-" (:id component))} - [:& render/component-svg {:root-shape (get-in component [:objects root-id]) + [:& render/component-svg {:class (stl/css :library-asset-icon) + :root-shape (get-in component [:objects root-id]) :objects (:objects component)}] ;; Components in the summary come loaded with objects, even in v2 - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :item-name) + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-item-name) :title (:name component)} (:name component)]]])) (when (> (:count components) (count (:sample components))) - [:div {:class (stl/css :asset-list-item)} - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :item-name)} "(...)"]]])]]) + [:div {:class (stl/css :library-asset-item)} + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-item-name)} "(...)"]]])]]) (when (pos? (:count colors)) - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.colors")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count colors) ")"]] ;; Unicode 00A0 is non-breaking space - [:div {:class (stl/css :asset-list)} + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") (:count colors) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :library-asset-list)} (for [color (:sample colors)] (let [default-name (cond (:gradient color) (uc/gradient-type->string (get-in color [:gradient :type])) (:color color) (:color color) :else (:value color))] - [:div {:class (stl/css :asset-list-item :color-item) + [:div {:class (stl/css :library-asset-item :library-color-item) :key (str "assets-color-" (:id color))} [:> bc/color-bullet* {:color {:color (:color color) :id (:id color) :opacity (:opacity color)} :mini true}] - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :color-name)} (:name color)] + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-color-name)} (:name color)] (when-not (= (:name color) default-name) - [:span {:class (stl/css :color-value)} (:color color)])]])) + [:span {:class (stl/css :library-color-value)} (:color color)])]])) (when (> (:count colors) (count (:sample colors))) - [:div {:class (stl/css :asset-list-item)} - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :item-name)} "(...)"]]])]]) + [:div {:class (stl/css :library-asset-item)} + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-item-name)} "(...)"]]])]]) (when (pos? (:count typographies)) - [:div {:class (stl/css :asset-section)} - [:div {:class (stl/css :asset-title)} + [:div {:class (stl/css :library-asset-section)} + [:div {:class (stl/css :library-asset-title)} [:span (tr "workspace.assets.typography")] - [:span {:class (stl/css :num-assets)} (str "\u00A0(") (:count typographies) ")"]] ;; Unicode 00A0 is non-breaking space - [:div {:class (stl/css :asset-list)} + [:span {:class (stl/css :library-num-assets)} (str "\u00A0(") (:count typographies) ")"]] ;; Unicode 00A0 is non-breaking space + [:div {:class (stl/css :library-asset-list)} (for [typography (:sample typographies)] - [:div {:class (stl/css :asset-list-item) + [:div {:class (stl/css :library-asset-item) :key (str "assets-typography-" (:id typography))} - [:div {:class (stl/css :typography-sample) + [:div {:class (stl/css :library-typography-sample) :style {:font-family (:font-family typography) :font-weight (:font-weight typography) :font-style (:font-style typography)}} (tr "workspace.assets.typography.sample")] - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :item-name) + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-item-name) :title (:name typography)} (:name typography)]]]) (when (> (:count typographies) (count (:sample typographies))) - [:div {:class (stl/css :asset-list-item)} - [:div {:class (stl/css :name-block)} - [:span {:class (stl/css :item-name)} "(...)"]]])]])]))]) + [:div {:class (stl/css :library-asset-item)} + [:div {:class (stl/css :library-name-block)} + [:span {:class (stl/css :library-item-name)} "(...)"]]])]])]))]) ;; --- Grid Item (mf/defc grid-item-metadata* - [{:keys [file]}] + {::mf/private true} + [{:keys [file layout]}] (let [time (ct/timeago (or (:will-be-deleted-at file) (:modified-at file)))] - [:span {:class (stl/css :date) + [:span {:class (stl/css-case :grid-item-date (= layout :grid) + :list-item-date (= layout :list)) :title (tr "dashboard.deleted.will-be-deleted-at" time)} time])) @@ -255,31 +254,32 @@ counter-el)) (mf/defc grid-item* - [{:keys [file origin can-edit selected-files can-restore]}] - (let [file-id (get file :id) - state (mf/deref refs/dashboard-local) + {::mf/private true} + [{:keys [file origin can-edit selected-files can-restore layout]}] + (let [node-ref (mf/use-ref) + menu-ref (mf/use-ref) - menu-pos - (get state :menu-pos) + state (mf/deref refs/dashboard-local) - menu-open? - (and (get state :menu-open) - (= file-id (:file-id state))) + file-id (get file :id) - selected? - (contains? selected-files file-id) + menu-pos (get state :menu-pos) + menu-open? (and (get state :menu-open) + (= file-id (:file-id state))) - selected-num - (count selected-files) + selected? (contains? selected-files file-id) + selected-num (count selected-files) - node-ref (mf/use-ref) - menu-ref (mf/use-ref) + list? (= layout :list) - is-library-view? - (= origin :libraries) + editing? (and (= file-id (:file-id state)) + (:edition state)) + + library-view? (= origin :libraries) on-menu-close - (mf/use-fn #(st/emit! (dd/hide-file-menu))) + (mf/use-fn + #(st/emit! (dd/hide-file-menu))) on-select (mf/use-fn @@ -335,29 +335,32 @@ on-menu-click (mf/use-fn - (mf/deps file selected?) + (mf/deps file selected? menu-open?) (fn [event] (dom/stop-propagation event) - (when-not selected? - (when-not (kbd/shift? event) - (st/emit! (dd/clear-selected-files))) + (if menu-open? + (st/emit! (dd/hide-file-menu)) + (do - (st/emit! (dd/toggle-file-select file)))) + (when-not selected? + (when-not (kbd/shift? event) + (st/emit! (dd/clear-selected-files))) + (st/emit! (dd/toggle-file-select file))) - (let [client-position - (dom/get-client-position event) + (let [client-position + (dom/get-client-position event) - position - (if (and (nil? (:y client-position)) (nil? (:x client-position))) - (let [target-element (dom/get-target event) - points (dom/get-bounding-rect target-element) - y (:top points) - x (:left points)] - (gpt/point x y)) - client-position)] + position + (if (and (nil? (:y client-position)) (nil? (:x client-position))) + (let [target-element (dom/get-target event) + points (dom/get-bounding-rect target-element) + y (:top points) + x (:left points)] + (gpt/point x y)) + client-position)] - (st/emit! (dd/show-file-menu-with-position file-id position))))) + (st/emit! (dd/show-file-menu-with-position file-id position))))))) on-context-menu (mf/use-fn @@ -390,7 +393,10 @@ (when (kbd/enter? event) (on-navigate event)) (when (kbd/shift? event) - (when (or (kbd/down-arrow? event) (kbd/left-arrow? event) (kbd/up-arrow? event) (kbd/right-arrow? event)) + (when (or (kbd/down-arrow? event) + (kbd/left-arrow? event) + (kbd/up-arrow? event) + (kbd/right-arrow? event)) ;; TODO Fix this (on-select event))))) @@ -401,73 +407,117 @@ (when (kbd/enter? event) (dom/stop-propagation event) (dom/prevent-default event) - (on-menu-click event))))] + (on-menu-click event)))) - [:li {:class (stl/css-case :grid-item true - :project-th true - :library is-library-view?)} - [:div - {:class (stl/css-case :selected selected? - :library is-library-view?) - :ref node-ref - :role "button" - :title (:name file) - :aria-label (:name file) - :draggable (dm/str can-edit) - :on-click on-select - :on-key-down on-key-down - :on-double-click on-navigate - :on-drag-start on-drag-start - :on-context-menu on-context-menu} + ;; The options menu is identical in both layouts, so we build it once + ;; and place it where each layout needs it. NOTE: hiccup bound in a + ;; let is not compiled by rumext, so it must be wrapped in mf/html. + menu-element + (mf/html + [:div {:class (stl/css-case :project-thumbnail-actions true + :is-force-display menu-open?)} + [:div {:class (stl/css :project-thumbnail-icon :menu) + :tab-index "0" + :role "button" + :aria-label (tr "dashboard.options") + :ref menu-ref + :id (dm/str file-id "-action-menu") + :on-click on-menu-click + :on-key-down on-menu-key-down} - [:div {:class (stl/css :overlay)}] + [:> icon* {:icon-id i/menu + :class (stl/css :menu-icon)}] - (if ^boolean is-library-view? - [:> grid-item-library* {:file file :can-restore can-restore}] - [:> grid-item-thumbnail* {:file file :can-edit can-edit :can-restore can-restore}]) + (when (and selected? menu-open?) + ;; When the menu is open we disable events in the dashboard. We need to force pointer events + ;; so the menu can be handled + [:> portal-on-document* {} + [:> file-menu* {:files (vals selected-files) + :left (+ 24 (:x menu-pos)) + :top (:y menu-pos) + :can-edit can-edit + :navigate true + :on-edit on-edit + :on-close on-menu-close + :origin origin + :parent-id (dm/str file-id "-action-menu") + :can-restore can-restore}]])]])] - (when (and (:is-shared file) (not is-library-view?)) - [:div {:class (stl/css :item-badge)} deprecated-icon/library]) + (if ^boolean list? + [:li {:class (stl/css-case :grid-item true + :list-item true + :library-item library-view?)} + [:div + {:class (stl/css-case :list-item-row true + :is-selected selected?) + :ref node-ref + :role "button" + :title (:name file) + :aria-label (:name file) + :draggable (dm/str can-edit) + :on-click on-select + :on-key-down on-key-down + :on-double-click on-navigate + :on-drag-start on-drag-start + :on-context-menu on-context-menu} - [:div {:class (stl/css :info-wrapper)} - [:div {:class (stl/css :item-info)} - (if (and (= file-id (:file-id state)) (:edition state)) + (if ^boolean editing? [:& inline-edition {:content (:name file) :on-end edit :max-length 250}] - [:h3 (:name file)]) - [:> grid-item-metadata* {:file file}]] + [:h3 {:class (stl/css :list-item-name)} (:name file)]) - [:div {:class (stl/css-case :project-th-actions true :force-display menu-open?)} - [:div - {:class (stl/css :project-th-icon :menu) - :tab-index "0" - :role "button" - :aria-label (tr "dashboard.options") - :ref menu-ref - :id (dm/str file-id "-action-menu") - :on-click on-menu-click - :on-key-down on-menu-key-down} + (when (and (:is-shared file) (not library-view?)) + [:span {:class (stl/css :list-item-badge) + :aria-label (tr "workspace.assets.shared-library") + :title (tr "workspace.assets.shared-library")} + [:> icon* {:icon-id i/library}]]) - menu-icon - (when (and selected? menu-open?) - ;; When the menu is open we disable events in the dashboard. We need to force pointer events - ;; so the menu can be handled - [:> portal-on-document* {} - [:> file-menu* {:files (vals selected-files) - :left (+ 24 (:x menu-pos)) - :top (:y menu-pos) - :can-edit can-edit - :navigate true - :on-edit on-edit - :on-close on-menu-close - :origin origin - :parent-id (dm/str file-id "-action-menu") - :can-restore can-restore}]])]]]]])) + [:> grid-item-metadata* {:file file :layout :list}] + + menu-element]] + + [:li {:class (stl/css-case :grid-item true + :project-thumbnail true + :library-item library-view?)} + [:div {:class (stl/css-case :is-selected selected?) + :ref node-ref + :role "button" + :title (:name file) + :aria-label (:name file) + :draggable (dm/str can-edit) + :on-click on-select + :on-key-down on-key-down + :on-double-click on-navigate + :on-drag-start on-drag-start + :on-context-menu on-context-menu} + + (if ^boolean library-view? + [:> grid-item-library* {:file file + :can-restore can-restore}] + [:> grid-item-thumbnail* {:file file + :can-edit can-edit + :can-restore can-restore}]) + + (when (and (:is-shared file) (not library-view?)) + [:div {:class (stl/css :grid-item-badge)} + [:> icon* {:icon-id i/library}]]) + + [:div {:class (stl/css :grid-item-info)} + [:div {:class (stl/css :grid-item-meta)} + (if ^boolean editing? + [:& inline-edition {:content (:name file) + :on-end edit + :max-length 250}] + [:h3 {:class (stl/css :grid-item-title)} (:name file)]) + [:> grid-item-metadata* {:file file :layout :grid}]] + + menu-element]]]))) (mf/defc grid* - [{:keys [files project origin limit create-fn can-edit selected-files can-restore]}] + [{:keys [files project origin limit create-fn can-edit selected-files can-restore layout]}] (let [dragging? (mf/use-state false) + list? (= layout :list) project-id (get project :id) team-id (get project :team-id) @@ -534,6 +584,19 @@ (nil? files) [:> loading-placeholder*] + (and (seq files) list?) + [:ul {:class (stl/css :grid-row :list-view)} + (when @dragging? + [:li {:class (stl/css :list-item-dragged)}]) + (for [item files] + [:> grid-item* {:file item + :key (dm/str (:id item)) + :origin origin + :selected-files selected-files + :can-edit can-edit + :can-restore can-restore + :layout :list}])] + (seq files) (for [[index slice] (d/enumerate (partition-all limit files))] @@ -541,45 +604,57 @@ (when @dragging? [:li {:class (stl/css :grid-item)}]) (for [item slice] - [:> grid-item* - {:file item - :key (dm/str (:id item)) - :origin origin - :selected-files selected-files - :can-edit can-edit - :can-restore can-restore}])]) + [:> grid-item* {:file item + :key (dm/str (:id item)) + :origin origin + :selected-files selected-files + :can-edit can-edit + :can-restore can-restore}])]) :else - [:> empty-grid-placeholder* - {:limit limit - :can-edit can-edit - :create-fn create-fn - :origin origin - :project-id project-id - :team-id team-id - :on-finish-import on-finish-import}])])) + [:> empty-grid-placeholder* {:limit limit + :can-edit can-edit + :create-fn create-fn + :origin origin + :project-id project-id + :team-id team-id + :on-finish-import on-finish-import}])])) -(mf/defc line-grid-row - [{:keys [files selected-files dragging? limit can-edit can-restore] :as props}] +(mf/defc line-grid-row* + {::mf/private true} + [{:keys [files selected-files is-dragging limit can-edit can-restore layout]}] (let [elements limit - limit (if dragging? (dec limit) limit)] - [:ul {:class (stl/css :grid-row :no-wrap) - :style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}} + limit (if is-dragging (dec limit) limit) + list? (= layout :list)] + (if ^boolean list? + [:ul {:class (stl/css :grid-row :list-view)} + (when is-dragging + [:li {:class (stl/css :list-item-dragged)}]) + (for [item (take limit files)] + [:> grid-item* {:id (:id item) + :file item + :selected-files selected-files + :can-edit can-edit + :key (dm/str (:id item)) + :can-restore can-restore + :layout :list}])] - (when dragging? - [:li {:class (stl/css :grid-item :dragged)}]) + [:ul {:class (stl/css :grid-row :no-wrap) + :style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}} - (for [item (take limit files)] - [:> grid-item* - {:id (:id item) - :file item - :selected-files selected-files - :can-edit can-edit - :key (dm/str (:id item)) - :can-restore can-restore}])])) + (when is-dragging + [:li {:class (stl/css :grid-item :is-dragged)}]) -(mf/defc line-grid - [{:keys [project team files limit create-fn can-edit can-restore] :as props}] + (for [item (take limit files)] + [:> grid-item* {:id (:id item) + :file item + :selected-files selected-files + :can-edit can-edit + :key (dm/str (:id item)) + :can-restore can-restore}])]))) + +(mf/defc line-grid* + [{:keys [project team files limit create-fn can-edit can-restore layout]}] (let [dragging? (mf/use-state false) project-id (:id project) team-id (:id team) @@ -672,20 +747,20 @@ [:> loading-placeholder*] (seq files) - [:& line-grid-row {:files files - :team-id team-id - :selected-files selected-files - :dragging? @dragging? - :can-edit can-edit - :limit limit - :can-restore can-restore}] + [:> line-grid-row* {:files files + :team-id team-id + :selected-files selected-files + :is-dragging @dragging? + :can-edit can-edit + :limit limit + :can-restore can-restore + :layout layout}] :else - [:> empty-grid-placeholder* - {:is-dragging @dragging? - :limit limit - :can-edit can-edit - :create-fn create-fn - :project-id project-id - :team-id team-id - :on-finish-import on-finish-import}])])) + [:> empty-grid-placeholder* {:is-dragging @dragging? + :limit limit + :can-edit can-edit + :create-fn create-fn + :project-id project-id + :team-id team-id + :on-finish-import on-finish-import}])])) diff --git a/frontend/src/app/main/ui/dashboard/grid.scss b/frontend/src/app/main/ui/dashboard/grid.scss index 66df5339f4..448600c835 100644 --- a/frontend/src/app/main/ui/dashboard/grid.scss +++ b/frontend/src/app/main/ui/dashboard/grid.scss @@ -4,247 +4,214 @@ // // Copyright (c) KALEIDOS INC Sucursal en España SL -@use "refactor/common-refactor.scss" as deprecated; +@use "ds/_borders.scss" as *; +@use "ds/_sizes.scss" as *; +@use "ds/_utils.scss" as *; +@use "ds/spacing.scss" as *; +@use "ds/typography.scss" as t; +@use "ds/z-index.scss" as *; + +// ─── VARIABLES ───────────────────────────────── -// TODO: Legacy sass variables. We should remove them in favor of DS tokens. $bp-max-1366: "(max-width: 1366px)"; -$thumbnail-default-width: deprecated.$s-252; // Default width -$thumbnail-default-height: deprecated.$s-168; // Default width +$thumbnail-default-width: $sz-252; +$thumbnail-default-height: px2rem(168); + +// ─── DASHBOARD GRID ──────────────────────────── .dashboard-grid { - font-size: deprecated.$fs-14; - height: 100%; + @include t.use-typography("body-medium"); + + block-size: 100%; overflow: hidden auto; - padding: 0 var(--sp-l) deprecated.$s-16; + padding: 0 var(--sp-l) var(--sp-l); } +// ─── GRID ROW ────────────────────────────────── + .grid-row { display: grid; grid-auto-flow: column; - grid-auto-columns: calc(deprecated.$s-12 + var(--th-width, #{$thumbnail-default-width})); - width: 100%; - gap: deprecated.$s-24; + grid-auto-columns: calc(var(--sp-m) + var(--thumbnail-width, #{$thumbnail-default-width})); + inline-size: 100%; + gap: var(--sp-xxl); + + &.list-view { + display: flex; + flex-direction: column; + grid-auto-flow: initial; + grid-auto-columns: initial; + gap: var(--sp-s); + padding: var(--sp-s) 0; + inline-size: 100%; + } } +// ─── CARD VIEW: GRID ITEM ───────────────────── + .grid-item { align-items: center; - cursor: pointer; display: flex; flex-direction: column; - margin: deprecated.$s-12 0; + margin: var(--sp-m) 0; position: relative; text-align: center; - a, - button { - width: 100%; - font-weight: deprecated.$fw400; - } - - button { - background-color: transparent; - border: none; - padding: 0 deprecated.$s-6; - } - - .grid-item-th { - border-radius: deprecated.$br-8; + &.is-dragged { + border-radius: $br-4; + outline: $br-4 solid var(--color-accent-primary); text-align: initial; - width: var(--th-width, #{$thumbnail-default-width}); - height: var(--th-height, #{$thumbnail-default-height}); - background-size: cover; - overflow: hidden; - - img { - object-fit: contain; - } - } - - &.dragged { - border-radius: deprecated.$br-4; - outline: deprecated.$br-4 solid var(--color-accent-primary); - text-align: initial; - width: calc(var(--th-width) + deprecated.$s-12); - height: var(--th-height, #{$thumbnail-default-height}); - } - - &.overlay { - border-radius: deprecated.$br-4; - border: deprecated.$s-2 solid var(--color-accent-tertiary); - height: 100%; - opacity: 0; - pointer-events: none; - position: absolute; - width: 100%; - z-index: deprecated.$z-index-1; - } - - &:hover .overlay { - display: block; - opacity: 1; - } - - .info-wrapper { - display: grid; - grid-template-columns: 1fr auto; - cursor: pointer; - max-width: var(--th-width, $thumbnail-default-width); - } - - .item-info { - display: grid; - padding: deprecated.$s-8; - text-align: left; - width: 100%; - font-size: deprecated.$fs-12; - - h3 { - border: deprecated.$s-1 solid transparent; - color: var(--color-foreground-primary); - font-size: deprecated.$fs-16; - font-weight: deprecated.$fw400; - height: deprecated.$s-28; - line-height: 1.92; - max-width: deprecated.$s-260; - overflow: hidden; - padding: 0; - text-overflow: ellipsis; - white-space: nowrap; - width: 100%; - - @media #{$bp-max-1366} { - max-width: deprecated.$s-232; - } - } - - .date { - color: var(--color-foreground-secondary); - overflow: hidden; - text-overflow: ellipsis; - width: 100%; - white-space: nowrap; - max-width: deprecated.$s-260; - - &::first-letter { - text-transform: capitalize; - } - - @media #{$bp-max-1366} { - max-width: deprecated.$s-232; - } - } - } - - .item-badge { - background-color: var(--color-accent-primary); - border: none; - border-radius: deprecated.$br-6; - position: absolute; - top: deprecated.$s-12; - right: deprecated.$s-12; - height: deprecated.$s-32; - width: deprecated.$s-32; - display: flex; - align-items: center; - justify-content: center; - - svg { - stroke: var(--color-background-secondary); - fill: none; - height: deprecated.$s-16; - width: deprecated.$s-16; - } - } - - &.add-file { - border: deprecated.$s-1 dashed var(--color-foreground-secondary); - justify-content: center; - box-shadow: none; - - span { - color: var(--color-background-primary); - font-size: deprecated.$fs-14; - } - - &:hover { - background-color: var(--color-foreground-primary); - border: deprecated.$s-2 solid var(--color-accent-tertiary); - } + inline-size: calc(var(--thumbnail-width) + var(--sp-m)); + block-size: var(--thumbnail-height, #{$thumbnail-default-height}); } } -.drag-counter { - position: absolute; - top: deprecated.$s-4; - left: deprecated.$s-4; - width: deprecated.$s-32; - height: deprecated.$s-32; - background-color: var(--color-accent-tertiary); - border-radius: deprecated.$br-circle; +.grid-item-info { + display: grid; + grid-template-columns: 1fr auto; + max-inline-size: var(--thumbnail-width, #{$thumbnail-default-width}); +} + +.grid-item-meta { + @include t.use-typography("body-small"); + + display: grid; + padding: var(--sp-s); + text-align: left; + inline-size: 100%; +} + +.grid-item-title { + @include t.use-typography("body-large"); + + border: $b-1 solid transparent; + color: var(--color-foreground-primary); + line-height: 1.92; + block-size: $sz-28; + max-inline-size: px2rem(260); + overflow: hidden; + padding: 0; + text-overflow: ellipsis; + white-space: nowrap; + inline-size: 100%; + + @media #{$bp-max-1366} { + max-inline-size: px2rem(232); + } +} + +.grid-item-date { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + overflow: hidden; + text-overflow: ellipsis; + inline-size: 100%; + white-space: nowrap; + max-inline-size: px2rem(260); + + &::first-letter { + text-transform: capitalize; + } + + @media #{$bp-max-1366} { + max-inline-size: px2rem(232); + } +} + +.grid-item-badge { color: var(--color-background-secondary); - font-size: deprecated.$fs-16; + background-color: var(--color-accent-primary); + border: none; + border-radius: $br-6; + position: absolute; + inset-block-start: var(--sp-m); + inset-inline-end: var(--sp-m); + block-size: $sz-32; + inline-size: $sz-32; display: flex; - justify-content: center; align-items: center; + justify-content: center; } -// PROJECTS, ELEMENTS & ICONS GRID -.project-th { +.grid-item-thumbnail { + border-radius: $br-8; + block-size: var(--thumbnail-height, #{$thumbnail-default-height}); + inline-size: var(--thumbnail-width, #{$thumbnail-default-width}); + overflow: hidden; + position: relative; + text-align: initial; + background-size: cover; + + &.is-deleted { + opacity: 0.5; + } +} + +.grid-item-thumbnail-image { + object-fit: contain; + block-size: auto; + inline-size: 100%; +} + +// ─── CARD VIEW: PROJECT THUMBNAIL ────────────── + +.project-thumbnail { background-color: transparent; - border-radius: deprecated.$br-8; - padding-top: deprecated.$s-6; + border-radius: $br-8; + padding-block-start: $sz-6; + + &.library-item { + block-size: px2rem(612); + } &:hover, &:focus, &:focus-within { background-color: var(--color-background-tertiary); - .project-th-actions { + .project-thumbnail-actions { opacity: 1; } - - a { - text-decoration: none; - } } - .selected { - .grid-item-th { - outline: deprecated.$s-4 solid var(--color-accent-tertiary); - } + .is-selected .grid-item-thumbnail { + outline: px2rem(4) solid var(--color-accent-tertiary); } } -.project-th-actions { +.project-thumbnail-actions { align-items: center; display: flex; - height: 100%; + block-size: 100%; justify-content: center; opacity: 0; - right: deprecated.$s-6; - width: deprecated.$s-32; + inset-inline-end: $sz-6; + inline-size: $sz-32; - span { - color: var(--color-background-secondary); + &.is-force-display { + opacity: 1; } } -.project-th-icon { +.project-thumbnail-icon { align-items: center; display: flex; - margin-right: deprecated.$s-8; - margin-top: 0; + margin-inline-end: var(--sp-s); + margin-block-start: 0; } +// ─── CARD VIEW: MENU ─────────────────────────── + .menu { align-items: flex-end; display: flex; flex-direction: column; - height: deprecated.$s-32; + block-size: $sz-32; justify-content: center; - margin-right: 0; - margin-top: deprecated.$s-20; - width: 100%; + margin-inline-end: 0; + margin-block-start: var(--sp-xl); + inline-size: 100%; --menu-icon-color: var(--button-tertiary-foreground-color-rest); @@ -255,130 +222,219 @@ $thumbnail-default-height: deprecated.$s-168; // Default width } .menu-icon { - stroke: var(--menu-icon-color); - fill: none; - margin-right: 0; - height: deprecated.$s-16; - width: deprecated.$s-16; + color: var(--menu-icon-color); + margin-inline-end: 0; + block-size: $sz-16; + inline-size: $sz-16; } -.project-th-actions.force-display { - opacity: 1; -} +// ─── LIST VIEW ───────────────────────────────── -.grid-item-th { - border-radius: deprecated.$br-4; - cursor: pointer; - height: 100%; - overflow: hidden; - position: relative; - width: 100%; - display: flex; - justify-content: center; +.list-item { + align-items: stretch; flex-direction: row; + margin: 0; + text-align: initial; + inline-size: 100%; - .img-th { - height: auto; - width: 100%; - } -} - -// LIBRARY VIEW -.library { - height: deprecated.$s-580; -} - -.grid-item.project-th.library { - height: deprecated.$s-612; -} - -.grid-item-th.library { - background-color: var(--color-background-tertiary); - flex-direction: column; - height: 90%; - justify-content: flex-start; - max-height: deprecated.$s-580; - padding: deprecated.$s-32; - - .asset-section { - font-size: deprecated.$fs-12; - color: var(--color-foreground-secondary); - - &:not(:first-child) { - margin-top: deprecated.$s-16; - } + // Reuse the grid options menu, but neutralize the card-oriented offsets so + // it sits inline at the end of the row. + .project-thumbnail-actions { + flex: 0 0 auto; + block-size: auto; + opacity: 1; + inline-size: auto; } - .asset-title { - display: flex; - font-size: deprecated.$fs-12; - text-transform: uppercase; - - .num-assets { - color: var(--color-foreground-secondary); - } + .project-thumbnail-icon { + margin: 0; } - .asset-list-item { + .menu { align-items: center; - border-radius: deprecated.$br-4; - border: deprecated.$s-1 solid transparent; - color: var(--color-foreground-primary); - display: flex; - font-size: deprecated.$fs-12; - margin-top: deprecated.$s-4; - padding: deprecated.$s-2; - position: relative; - - .name-block { - color: var(--color-foreground-secondary); - width: calc(100% - deprecated.$s-24 - deprecated.$s-8); - } - - .item-name { - display: block; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - } - - svg { - background-color: var(--color-canvas); - border-radius: deprecated.$br-4; - border: deprecated.$s-2 solid transparent; - height: deprecated.$s-24; - margin-right: deprecated.$s-8; - width: deprecated.$s-24; - } - - .color-name { - color: var(--color-foreground-primary); - } - - .color-value { - color: var(--color-foreground-secondary); - margin-left: deprecated.$s-4; - text-transform: uppercase; - } - - .typography-sample { - height: deprecated.$s-20; - margin-right: deprecated.$s-4; - width: deprecated.$s-20; - } + flex-direction: row; + block-size: $sz-32; + margin: 0; + inline-size: auto; } } -.color-item { +.list-item-dragged { + border-radius: $br-8; + block-size: $sz-48; + outline: $br-4 solid var(--color-accent-primary); + outline-offset: -$br-4; + inline-size: 100%; +} + +.list-item-row { + align-items: center; + border-radius: $br-8; + border: $b-1 solid var(--color-background-quaternary); + display: flex; + gap: var(--sp-m); + padding: var(--sp-s) var(--sp-m); + inline-size: 100%; + + &:hover, + &:focus, + &:focus-within { + background-color: var(--color-background-tertiary); + } + + &.is-selected { + background-color: var(--color-background-tertiary); + outline: $b-2 solid var(--color-accent-tertiary); + } +} + +.list-item-name { + @include t.use-typography("body-medium"); + + color: var(--color-foreground-primary); + flex: 0 1 auto; + margin: 0; + min-inline-size: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.list-item-badge { + align-items: center; + color: var(--color-background-secondary); + background-color: var(--color-accent-primary); + border-radius: $br-6; + display: flex; + flex: 0 0 auto; + justify-content: center; + block-size: $sz-24; + inline-size: $sz-24; +} + +.list-item-date { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + flex: 0 0 auto; + margin-inline-start: auto; + white-space: nowrap; + + &::first-letter { + text-transform: capitalize; + } +} + +// ─── LIBRARY ─────────────────────────────────── + +.library-thumbnail { + border-radius: $br-4; + position: relative; + overflow: hidden; + background-color: var(--color-background-tertiary); + display: flex; + flex-direction: column; + block-size: 90%; + justify-content: flex-start; + max-block-size: px2rem(580); + padding: var(--sp-xxxl); +} + +.library-num-assets { + color: var(--color-foreground-secondary); +} + +.library-name-block { + color: var(--color-foreground-secondary); + inline-size: calc(100% - var(--sp-xxl) - var(--sp-s)); +} + +.library-item-name { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.library-asset-section { + @include t.use-typography("body-small"); + + color: var(--color-foreground-secondary); + + &:not(:first-child) { + margin-block-start: var(--sp-l); + } +} + +.library-asset-item { + @include t.use-typography("body-small"); + + display: flex; + align-items: center; + border-radius: $br-4; + border: $b-1 solid transparent; + color: var(--color-foreground-primary); + margin-block-start: var(--sp-xs); + padding: var(--sp-xxs); + position: relative; +} + +.library-asset-title { + @include t.use-typography("body-small"); + + display: flex; + text-transform: uppercase; +} + +.library-asset-icon { + background-color: var(--color-canvas); + border-radius: $br-4; + border: $b-2 solid transparent; + margin-inline-end: var(--sp-s); + block-size: $sz-24; + inline-size: $sz-24; +} + +.library-color-name { + color: var(--color-foreground-primary); +} + +.library-color-value { + color: var(--color-foreground-secondary); + margin-inline-start: var(--sp-xs); + text-transform: uppercase; +} + +.library-color-item { display: grid; grid-template-columns: auto 1fr; - gap: deprecated.$s-8; + gap: var(--sp-s); +} + +.library-typography-sample { + block-size: px2rem(20); + margin-inline-end: var(--sp-xs); + inline-size: px2rem(20); +} + +// ─── MISC ────────────────────────────────────── + +.drag-counter { + @include t.use-typography("body-large"); + + position: absolute; + inset-block-start: var(--sp-xs); + inset-inline-start: var(--sp-xs); + inline-size: $sz-32; + block-size: $sz-32; + background-color: var(--color-accent-tertiary); + border-radius: $br-circle; + color: var(--color-background-secondary); + display: flex; + justify-content: center; + align-items: center; } .grid-loader { - --icon-width: calc(var(--th-width, #{$thumbnail-default-width}) * 0.25); -} - -.deleted-item { - opacity: 0.5; + --icon-width: calc(var(--thumbnail-width, #{$thumbnail-default-width}) * 0.25); } diff --git a/frontend/src/app/main/ui/dashboard/layout_toggle.cljs b/frontend/src/app/main/ui/dashboard/layout_toggle.cljs new file mode 100644 index 0000000000..75bd938cbe --- /dev/null +++ b/frontend/src/app/main/ui/dashboard/layout_toggle.cljs @@ -0,0 +1,32 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS INC Sucursal en España SL + +(ns app.main.ui.dashboard.layout-toggle + "Reactive, persisted preference for how dashboard files are laid out + (as a grid of thumbnails or as a compact list). The preference is shared + between the team (projects) view and the project (files) view." + (:require + [app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]] + [app.main.ui.ds.foundations.assets.icon :as i] + [app.util.i18n :refer [tr]] + [rumext.v2 :as mf])) + +(def layout-key ::dashboard-layout) +(def default-layout :grid) + +(mf/defc layout-toggle* + [{:keys [layout on-change]}] + [:> radio-buttons* {:selected (name layout) + :on-change on-change + :name "dashboard-files-layout" + :options [{:id "dashboard-files-layout-list" + :value "list" + :icon i/view-as-list + :label (tr "dashboard.files-layout.list")} + {:id "dashboard-files-layout-grid" + :value "grid" + :icon i/view-as-icons + :label (tr "dashboard.files-layout.grid")}]}]) diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 1c47e099b9..4161db7658 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -103,7 +103,7 @@ [:ul {:class (stl/css :grid-row :no-wrap) :style {:grid-template-columns (str "repeat(" limit ", 1fr)")}} - [:li {:class (stl/css :grid-item :grid-empty-placeholder :dragged)}]] + [:li {:class (stl/css :grid-item :grid-empty-placeholder :is-dragged)}]] (= :libraries origin) [:> empty-placeholder* diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss index da0355bba4..cf17521433 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.scss +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -49,8 +49,8 @@ cursor: pointer; margin: deprecated.$s-8; border: deprecated.$s-2 solid transparent; - width: var(--th-width, #{g.$thumbnail-default-width}); - height: var(--th-height, #{g.$thumbnail-default-height}); + width: var(--thumbnail-width, #{g.$thumbnail-default-width}); + height: var(--thumbnail-height, #{g.$thumbnail-default-height}); svg { width: deprecated.$s-32; diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index ee9ca03325..56e84ad53c 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -19,8 +19,9 @@ [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.dashboard.deleted :as deleted] - [app.main.ui.dashboard.grid :refer [line-grid]] + [app.main.ui.dashboard.grid :refer [line-grid*]] [app.main.ui.dashboard.inline-edition :refer [inline-edition]] + [app.main.ui.dashboard.layout-toggle :as lt :refer [layout-toggle*]] [app.main.ui.dashboard.pin-button :refer [pin-button*]] [app.main.ui.dashboard.project-menu :refer [project-menu*]] [app.main.ui.ds.buttons.button :refer [button*]] @@ -50,16 +51,20 @@ (mf/defc header* {::mf/wrap [mf/memo] ::mf/private true} - [{:keys [can-edit]}] + [{:keys [can-edit layout on-change]}] (let [on-click (mf/use-fn #(st/emit! (dd/create-project)))] - [:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"} + [:header {:class (stl/css :dashboard-header) + :data-testid "dashboard-header"} [:div#dashboard-projects-title {:class (stl/css :dashboard-title)} [:h1 (tr "dashboard.projects-title")]] - (when can-edit - [:button {:class (stl/css :btn-secondary :btn-small) - :on-click on-click - :data-testid "new-project-button"} - (tr "dashboard.new-project")])])) + [:div {:class (stl/css :dashboard-header-actions)} + [:> layout-toggle* {:layout layout + :on-change on-change}] + (when can-edit + [:button {:class (stl/css :btn-secondary :btn-small) + :on-click on-click + :data-testid "new-project-button"} + (tr "dashboard.new-project")])]])) (mf/defc team-hero* {::mf/wrap [mf/memo]} @@ -99,7 +104,7 @@ (mf/defc project-item* {::mf/private true} - [{:keys [project is-first team files can-edit]}] + [{:keys [project is-first team files can-edit layout]}] (let [project-id (get project :id) team-id (get team :id) @@ -285,13 +290,13 @@ (tr "dashboard.empty-placeholder-drafts-subtitle") (tr "dashboard.empty-placeholder-files-subtitle"))}] - [:& line-grid - {:project project - :team team - :files files - :create-fn create-file - :can-edit can-edit - :limit limit}])] + [:> line-grid* {:project project + :team team + :files files + :create-fn create-file + :can-edit can-edit + :limit limit + :layout layout}])] (when (and (> limit 0) (> file-count limit)) @@ -329,6 +334,14 @@ show-deleted? (:can-edit permisions) + layout* (hooks/use-persisted-state lt/layout-key lt/default-layout) + layout (deref layout*) + + on-layout-change + (mf/use-fn + (fn [value] + (reset! layout* (keyword value)))) + projects (mf/with-memo [projects] (->> projects @@ -360,7 +373,9 @@ (when (seq projects) [:* - [:> header* {:can-edit can-edit}] + [:> header* {:can-edit can-edit + :layout layout + :on-change on-layout-change}] [:div {:class (stl/css :projects-container)} [:* (when (and show-team-hero? @@ -377,7 +392,8 @@ can-invite))} (when show-deleted? - [:> deleted/menu* {:team-id team-id :section :dashboard-recent}]) + [:> deleted/menu* {:team-id team-id + :section :dashboard-recent}]) (for [{:keys [id] :as project} projects] ;; FIXME: refactor this, looks inneficient @@ -389,5 +405,6 @@ :team team :files files :can-edit can-edit + :layout layout :is-first (= project (first projects)) :key id}]))]]]]))) diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index ae8ebd30d5..ed13c36e4e 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -438,8 +438,8 @@ [th-size] (when th-size (let [node (mf/ref-val rowref)] - (.setProperty (.-style node) "--th-width" (str th-size "px")) - (.setProperty (.-style node) "--th-height" (str (mth/ceil (* th-size (/ 2 3))) "px"))))) + (.setProperty (.-style node) "--thumbnail-width" (str th-size "px")) + (.setProperty (.-style node) "--thumbnail-height" (str (mth/ceil (* th-size (/ 2 3))) "px"))))) (mf/with-effect [] (let [node (mf/ref-val rowref) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ea0d60d7ec..19767f7192 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -490,6 +490,14 @@ msgstr "Once a project member creates a file, it will be displayed here." msgid "dashboard.empty-placeholder-files-title" msgstr "No files yet." +#: src/app/main/ui/dashboard/files_layout.cljs +msgid "dashboard.files-layout.grid" +msgstr "Grid view" + +#: src/app/main/ui/dashboard/files_layout.cljs +msgid "dashboard.files-layout.list" +msgstr "List view" + #: src/app/main/ui/dashboard/placeholder.cljs:117 #, markdown msgid "dashboard.empty-placeholder-libraries" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index d117f9ca38..9bfa667435 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -495,6 +495,14 @@ msgstr "Cuando un miembro del equipo cree algún archivo, este aparecerá aquí. msgid "dashboard.empty-placeholder-files-title" msgstr "Aún no hay archivos." +#: src/app/main/ui/dashboard/files_layout.cljs +msgid "dashboard.files-layout.grid" +msgstr "Vista de cuadrícula" + +#: src/app/main/ui/dashboard/files_layout.cljs +msgid "dashboard.files-layout.list" +msgstr "Vista de lista" + #: src/app/main/ui/dashboard/placeholder.cljs:117 #, markdown msgid "dashboard.empty-placeholder-libraries"