diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 2b1a049de1..0ed40e3e2e 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -1148,12 +1148,3 @@ :position :right}) (ptk/event ::ev/event {::ev/name "add-asset-to-library" :asset-type "color"})))))) - -(defn get-active-color-tab - [] - (let [tab (::tab storage/user)] - (or tab :ramp))) - -(defn set-active-color-tab! - [tab] - (swap! storage/user assoc ::tab tab)) diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.cljs b/frontend/src/app/main/ui/ds/layout/tab_switcher.cljs index 78f837fe86..af232e255a 100644 --- a/frontend/src/app/main/ui/ds/layout/tab_switcher.cljs +++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.cljs @@ -6,29 +6,29 @@ (ns app.main.ui.ds.layout.tab-switcher (:require-macros - [app.common.data.macros :as dm] [app.main.style :as stl]) (:require + [app.common.data :as d] [app.main.ui.ds.foundations.assets.icon :refer [icon* icon-list]] - [app.util.array :as array] [app.util.dom :as dom] [app.util.keyboard :as kbd] [app.util.object :as obj] - [rumext.v2 :as mf])) + [rumext.v2 :as mf] + [rumext.v2.util :as mfu])) (mf/defc tab* {::mf/private true} - [{:keys [selected icon label aria-label id on-ref] :rest props}] - (let [class (stl/css-case :tab true - :selected selected) + [{:keys [selected icon label aria-label id ref] :rest props}] + (let [class (stl/css-case + :tab true + :selected selected) props (mf/spread-props props {:class class :role "tab" :aria-selected selected :title (or label aria-label) :tab-index (if selected nil -1) - :ref (fn [node] - (on-ref node id)) + :ref ref :data-id id ;; This prop is to be used for accessibility purposes only. :id id})] @@ -41,62 +41,57 @@ :aria-hidden (when label true) :aria-label (when (not label) aria-label)}]) (when (string? label) - [:span {:class (stl/css-case :tab-text true - :tab-text-and-icon icon)} + [:span {:class (stl/css-case + :tab-text true + :tab-text-and-icon icon)} label])]])) (mf/defc tab-nav* - {::mf/props :obj - ::mf/private true} - [{:keys [on-ref tabs selected on-click button-position action-button] :rest props}] - (let [class (stl/css-case :tab-nav true - :tab-nav-start (= "start" button-position) - :tab-nav-end (= "end" button-position)) - props (mf/spread-props props - {:class (stl/css :tab-list) - :role "tablist" - :aria-orientation "horizontal"})] - [:nav {:class class} + {::mf/private true + ::mf/memo true} + [{:keys [ref tabs selected on-click button-position action-button] :rest props}] + (let [nav-class + (stl/css-case :tab-nav true + :tab-nav-start (= "start" button-position) + :tab-nav-end (= "end" button-position)) + props + (mf/spread-props props + {:class (stl/css :tab-list) + :role "tablist" + :aria-orientation "horizontal"})] + [:nav {:class nav-class} (when (= button-position "start") action-button) - [:> "ul" props - (for [element ^js tabs] - (let [icon (obj/get element "icon") - label (obj/get element "label") - aria-label (obj/get element "aria-label") - id (obj/get element "id")] + [:> :ul props + (for [element tabs] + (let [icon (get element :icon) + label (get element :label) + aria-label (get element :aria-label) + id (get element :id)] [:> tab* {:icon icon - :key (dm/str "tab-" id) + :key id :label label :aria-label aria-label :selected (= id selected) :on-click on-click - :on-ref on-ref + :ref ref :id id}]))] (when (= button-position "end") action-button)])) -(defn- get-tab - [tabs id] - (or (array/find #(= id (obj/get % "id")) tabs) - (aget tabs 0))) - -(defn- get-selected-tab-id - [tabs default] - (let [tab (get-tab tabs default)] - (obj/get tab "id"))) +(def ^:private schema:tab-attrs + [:map {:title "tab"} + [:icon {:optional true} + [:and :string [:fn #(contains? icon-list %)]]] + [:label {:optional true} :string] + [:aria-label {:optional true} :string]]) (def ^:private schema:tab [:and - [:map {:title "tab"} - [:icon {:optional true} - [:and :string [:fn #(contains? icon-list %)]]] - [:label {:optional true} :string] - [:aria-label {:optional true} :string] - [:content some?]] + schema:tab-attrs [:fn {:error/message "invalid data: missing required props"} (fn [tab] (or (and (contains? tab :icon) @@ -108,95 +103,91 @@ [:map [:tabs [:vector {:min 1} schema:tab]] [:class {:optional true} :string] - [:on-change-tab {:optional true} fn?] - [:default-selected {:optional true} :string] - [:selected {:optional true} :string] + [:on-change fn?] + [:selected :string] [:action-button {:optional true} some?] [:action-button-position {:optional true} [:maybe [:enum "start" "end"]]]]) (mf/defc tab-switcher* - {::mf/props :obj - ::mf/schema schema:tab-switcher} - [{:keys [tabs class on-change-tab default-selected selected action-button-position action-button] :rest props}] - (let [selected* (mf/use-state #(or selected (get-selected-tab-id tabs default-selected))) - selected (or selected (deref selected*)) + {::mf/schema schema:tab-switcher} + [{:keys [tabs class on-change selected action-button-position action-button children] :rest props}] + (let [nodes-ref (mf/use-ref nil) + + tabs + (if (array? tabs) + (mfu/bean tabs) + tabs) - tabs-nodes-refs (mf/use-ref nil) - tabs-ref (mf/use-ref nil) on-click (mf/use-fn - (mf/deps on-change-tab) + (mf/deps on-change) (fn [event] (let [node (dom/get-current-target event) id (dom/get-data node "id")] - (reset! selected* id) - - (when (fn? on-change-tab) - (on-change-tab id))))) + (when (fn? on-change) + (on-change id))))) on-ref (mf/use-fn - (fn [node id] - (let [refs (or (mf/ref-val tabs-nodes-refs) #js {}) - refs (if node - (obj/set! refs id node) - (obj/unset! refs id))] - (mf/set-ref-val! tabs-nodes-refs refs)))) + (fn [node] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/set! state id node)] + (mf/set-ref-val! nodes-ref state) + (fn [] + (let [state (mf/ref-val nodes-ref) + state (d/nilv state #js {}) + id (dom/get-data node "id") + state (obj/unset! state id)] + (mf/set-ref-val! nodes-ref state)))))) on-key-down (mf/use-fn - (mf/deps selected) + (mf/deps selected tabs on-change) (fn [event] - (let [tabs (mf/ref-val tabs-ref) - len (alength tabs) - sel? #(= selected (obj/get % "id")) + (let [len (count tabs) + sel? #(= selected (get % :id)) id (cond (kbd/home? event) - (let [tab (aget tabs 0)] - (obj/get tab "id")) + (let [tab (nth tabs 0)] + (get tab :id)) (kbd/left-arrow? event) - (let [index (array/find-index sel? tabs) + (let [index (d/index-of-pred tabs sel?) index (mod (- index 1) len) - tab (aget tabs index)] - (obj/get tab "id")) + tab (nth tabs index)] + (get tab :id)) (kbd/right-arrow? event) - (let [index (array/find-index sel? tabs) + (let [index (d/index-of-pred tabs sel?) index (mod (+ index 1) len) - tab (aget tabs index)] - (obj/get tab "id")))] + tab (nth tabs index)] + (get tab :id)))] (when (some? id) - (reset! selected* id) - (let [nodes (mf/ref-val tabs-nodes-refs) + (on-change id) + (let [nodes (mf/ref-val nodes-ref) node (obj/get nodes id)] (dom/focus! node)))))) - class (dm/str class " " (stl/css :tabs)) - - props (mf/spread-props props {:class class})] - - (mf/with-effect [tabs] - (mf/set-ref-val! tabs-ref tabs)) + props + (mf/spread-props props {:class [class (stl/css :tabs)]})] [:> :article props [:div {:class (stl/css :padding-wrapper)} [:> tab-nav* {:button-position action-button-position :action-button action-button :tabs tabs - :on-ref on-ref + :ref on-ref :selected selected :on-key-down on-key-down :on-click on-click}]] - (let [active-tab (get-tab tabs selected) - content (obj/get active-tab "content") - id (obj/get active-tab "id")] - [:section {:class (stl/css :tab-panel) - :tab-index 0 - :role "tabpanel" - :aria-labelledby id} - content])])) + [:section {:class (stl/css :tab-panel) + :tab-index 0 + :role "tabpanel" + :aria-labelledby selected} + children]])) diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.mdx b/frontend/src/app/main/ui/ds/layout/tab_switcher.mdx index 2675bed01d..d145d33131 100644 --- a/frontend/src/app/main/ui/ds/layout/tab_switcher.mdx +++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.mdx @@ -49,21 +49,33 @@ These are available in the `app.main.ds.foundations.assets.icon` namespace. [app.main.ui.ds.foundations.assets.icon :as i])) ``` -```clj -[:> tab_switcher* - {:tabs [{ :label "Code" - :id "tab-code" - :icon i/fill-content - :content [:p Lorem Ipsum ]} - { :label "Design" - :id "tab-design" - :icon i/pentool - :content [:p Dolor sit amet ]} - { :label "Menu" - :id "tab-menu" - :icon i/mask - :content [:p Consectetur adipiscing elit ]} - ]}] +```clojure +(let [selected-tab* (mf/use-state "code") + selected-tab (deref selected-tab*) + on-change-tab (mf/use-fn #(reset! selected-tab* %)) + tabs (mf/with-memo [] + [{:label "Code" + :id "code" + :icon i/fill-content} + {:label "Design" + :id "design" + :icon i/pentool} + {:label "Menu" + :id "menu" + :icon i/mask}])] + [:> tab_switcher* {:tabs tabs + :selected selected-tab + :on-change on-change-tab} + (case selected-tab + "code" + [:p "Lorem Ipsum"] + + "design" + [:p "Dolor sit amet"] + + "menu" + [:p "Consectetur adipiscing elit"])]) + ``` diff --git a/frontend/src/app/main/ui/ds/layout/tab_switcher.stories.jsx b/frontend/src/app/main/ui/ds/layout/tab_switcher.stories.jsx index daaeada1a2..33840f8802 100644 --- a/frontend/src/app/main/ui/ds/layout/tab_switcher.stories.jsx +++ b/frontend/src/app/main/ui/ds/layout/tab_switcher.stories.jsx @@ -13,9 +13,30 @@ const Padded = ({ children }) => (
{children}
); +const TabSwitcherWrapper = ({tabs, ...props}) => { + const navTabs = tabs.map(({content, ...item}) => { + return item; + }); + + const [selected, setSelected] = React.useState(() => { + return props.default || tabs[0].id; + }); + + const content = tabs.reduce((result, tab) => { + result[tab.id] = tab.content; + return result; + }, {}); + + return ( + + {content[selected]} + + ); +}; + export default { title: "Layout/Tab switcher", - component: TabSwitcher, + component: TabSwitcherWrapper, args: { tabs: [ { @@ -46,7 +67,7 @@ export default { ), }, ], - defaultSelected: "tab-code", + default: "tab-code", }, argTypes: { actionButtonPosition: { @@ -59,12 +80,12 @@ export default { exclude: [ "tabs", "actionButton", - "defaultSelected", + "default", "actionButtonPosition", ], }, }, - render: ({ ...args }) => , + render: ({ ...args }) => , }; export const Default = {}; diff --git a/frontend/src/app/main/ui/hooks.cljs b/frontend/src/app/main/ui/hooks.cljs index 2458fbed67..bfc2399d2a 100644 --- a/frontend/src/app/main/ui/hooks.cljs +++ b/frontend/src/app/main/ui/hooks.cljs @@ -324,6 +324,22 @@ state*)) +(defn use-persisted-state + "A specialized hook that adds persistence to the default mf/use-state hook. + + The state is automatically persisted under the provided key on + localStorage. And it will keep watching events with type equals to + `key` for new values." + [key default] + (let [id (mf/use-id) + state* (mf/use-state #(get storage/user key default)) + state (deref state*)] + + (mf/with-effect [state key id] + (swap! storage/user assoc key state)) + + state*)) + (defonce ^:private intersection-subject (rx/subject)) (defonce ^:private intersection-observer (delay (js/IntersectionObserver. diff --git a/frontend/src/app/main/ui/inspect/right_sidebar.cljs b/frontend/src/app/main/ui/inspect/right_sidebar.cljs index 8ef94c8e0d..d732f90922 100644 --- a/frontend/src/app/main/ui/inspect/right_sidebar.cljs +++ b/frontend/src/app/main/ui/inspect/right_sidebar.cljs @@ -38,7 +38,7 @@ (mf/defc right-sidebar [{:keys [frame page objects file selected shapes page-id file-id share-id from on-change-section on-expand] :or {from :viewer}}] - (let [section (mf/use-state :info #_:code) + (let [section (mf/use-state #(do :info)) objects (or objects (:objects page)) shapes (or shapes (resolve-shapes objects selected)) @@ -81,31 +81,13 @@ (fn [] (dom/open-new-window "https://help.penpot.app/user-guide/inspect/"))) - info-content - (mf/html [:& attributes {:page-id page-id - :objects objects - :file-id file-id - :frame frame - :shapes shapes - :from from - :libraries libraries - :share-id share-id}]) - - code-content - (mf/html [:& code {:frame frame - :shapes shapes - :on-expand handle-expand - :from from}]) - tabs - #js [#js {:label (tr "inspect.tabs.info") - :id "info" - :content info-content} - - #js {:label (tr "inspect.tabs.code") - :data-testid "code" - :id "code" - :content code-content}]] + (mf/with-memo [] + [{:label (tr "inspect.tabs.info") + :id "info"} + {:label (tr "inspect.tabs.code") + :data-testid "code" + :id "code"}])] (mf/use-effect (mf/deps shapes handle-change-tab) @@ -148,9 +130,25 @@ [:div {:class (stl/css :inspect-content)} [:> tab-switcher* {:tabs tabs - :default-selected "info" - :on-change-tab handle-change-tab - :class (stl/css :viewer-tab-switcher)}]]] + :selected (name @section) + :on-change handle-change-tab + :class (stl/css :viewer-tab-switcher)} + (case @section + :info + [:& attributes {:page-id page-id + :objects objects + :file-id file-id + :frame frame + :shapes shapes + :from from + :libraries libraries + :share-id share-id}] + + :code + [:& code {:frame frame + :shapes shapes + :on-expand handle-expand + :from from}])]]] [:div {:class (stl/css :empty)} [:div {:class (stl/css :code-info)} [:span {:class (stl/css :placeholder-icon)} diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index a7a905a397..b2c0b1192f 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -59,6 +59,13 @@ {:keys [vport] :as wlocal} (mf/deref refs/workspace-local) {:keys [options-mode]} wglobal + + ;; FIXME: pass this down to viewport and reuse it from here + ;; instead of making an other deref on viewport for the same + ;; data + drawing + (mf/deref refs/workspace-drawing) + colorpalette? (:colorpalette layout) textpalette? (:textpalette layout) hide-ui? (:hide-ui layout) @@ -77,7 +84,7 @@ node-ref (use-resize-observer on-resize)] [:* - (when (not hide-ui?) + (when (not ^boolean hide-ui?) [:& palette {:layout layout :on-change-palette-size on-resize-palette}]) @@ -115,6 +122,7 @@ :page-id page-id}]) [:> right-sidebar* {:section options-mode :selected selected + :drawing-tool (get drawing :tool) :layout layout :file file :page-id page-id}]])])) @@ -147,7 +155,8 @@ (-> file (dissoc :data) (assoc ::has-data (contains? file :data)))))) - st/state)) + st/state + =)) (defn- make-page-ref [file-id page-id] diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index e5c5548bca..147aa74991 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -29,6 +29,7 @@ [app.main.ui.components.select :refer [select]] [app.main.ui.ds.foundations.assets.icon :as ic] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] + [app.main.ui.hooks :as hooks] [app.main.ui.icons :as i] [app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]] [app.main.ui.workspace.colorpicker.gradients :refer [gradients*]] @@ -112,11 +113,14 @@ :linear :linear-gradient :radial :radial-gradient) :color)) - active-color-tab (mf/use-state #(dc/get-active-color-tab)) + + active-color-tab* (hooks/use-persisted-state ::color-tab "ramp") + active-color-tab (deref active-color-tab*) + drag?* (mf/use-state false) drag? (deref drag?*) - type (if (= @active-color-tab "hsva") :hsv :rgb) + type (if (= active-color-tab "hsva") :hsv :rgb) fill-image-ref (mf/use-ref nil) @@ -163,10 +167,7 @@ :checked keep-aspect-ratio?}))))) on-change-tab - (mf/use-fn - (fn [tab] - (reset! active-color-tab tab) - (dc/set-active-color-tab! tab))) + (mf/use-fn #(reset! active-color-tab* %)) handle-change-mode (mf/use-fn @@ -282,12 +283,10 @@ handle-stop-edit-start (mf/use-fn - (fn [] - (reset! should-update? false))) + #(reset! should-update? false)) handle-stop-edit-finish (mf/use-fn - (mf/deps state) (fn [] (reset! should-update? true) @@ -338,50 +337,24 @@ (fn [value] (st/emit! (dc/update-colorpicker-gradient-opacity (/ value 100))))) - cap-stops? (or (features/use-feature "render-wasm/v1") (contains? cfg/flags :frontend-binary-fills)) + render-wasm? + (features/use-feature "render-wasm/v1") + + cap-stops? + (or ^boolean render-wasm? + ^boolean (contains? cfg/flags :frontend-binary-fills)) tabs - #js [#js {:aria-label (tr "workspace.libraries.colors.rgba") - :icon ic/rgba - :id "ramp" - :content (mf/html (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] - [:> ramp-selector* - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]))} - - #js {:aria-label "Harmony" - :icon ic/rgba-complementary - :id "harmony" - :content (mf/html (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] - [:& harmony-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]))} - - #js {:aria-label "HSVA" - :icon ic/hsva - :id "hsva" - :content (mf/html (if picking-color? - [:div {:class (stl/css :picker-detail-wrapper)} - [:div {:class (stl/css :center-circle)}] - [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] - [:& hsva-selector - {:color current-color - :disable-opacity disable-opacity - :on-change handle-change-color - :on-start-drag on-start-drag - :on-finish-drag on-finish-drag}]))}]] + (mf/with-memo [] + [{:aria-label (tr "workspace.libraries.colors.rgba") + :icon ic/rgba + :id "ramp"} + {:aria-label "Harmony" + :icon ic/rgba-complementary + :id "harmony"} + {:aria-label "HSVA" + :icon ic/hsva + :id "hsva"}])] ;; Initialize colorpicker state (mf/with-effect [] @@ -486,11 +459,41 @@ :multi false :ref fill-image-ref :on-selected on-fill-image-selected}]]]) + [:* [:div {:class (stl/css :colorpicker-tabs)} [:> tab-switcher* {:tabs tabs - :default-selected "ramp" - :on-change-tab on-change-tab}]] + :selected active-color-tab + :on-change on-change-tab} + (if picking-color? + [:div {:class (stl/css :picker-detail-wrapper)} + [:div {:class (stl/css :center-circle)}] + [:canvas#picker-detail {:class (stl/css :picker-detail) :width 256 :height 140}]] + + + (case active-color-tab + "ramp" + [:> ramp-selector* + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}] + + "harmony" + [:& harmony-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag}] + + "hsva" + [:& hsva-selector + {:color current-color + :disable-opacity disable-opacity + :on-change handle-change-color + :on-start-drag on-start-drag + :on-finish-drag on-finish-drag}]))]] [:& color-inputs {:type type diff --git a/frontend/src/app/main/ui/workspace/left_header.cljs b/frontend/src/app/main/ui/workspace/left_header.cljs index 875907f016..44cad87617 100644 --- a/frontend/src/app/main/ui/workspace/left_header.cljs +++ b/frontend/src/app/main/ui/workspace/left_header.cljs @@ -26,8 +26,7 @@ ;; --- Header Component -(mf/defc left-header - {::mf/props :obj} +(mf/defc left-header* [{:keys [file layout project page-id class]}] (let [profile (mf/deref refs/profile) file-id (:id file) diff --git a/frontend/src/app/main/ui/workspace/libraries.cljs b/frontend/src/app/main/ui/workspace/libraries.cljs index e00c06fc35..dcc676201b 100644 --- a/frontend/src/app/main/ui/workspace/libraries.cljs +++ b/frontend/src/app/main/ui/workspace/libraries.cljs @@ -565,7 +565,7 @@ (mf/defc libraries-dialog {::mf/register modal/components ::mf/register-as :libraries-dialog} - [{:keys [starting-tab file-id] :as props :or {starting-tab :libraries}}] + [{:keys [starting-tab file-id]}] (let [files (mf/deref refs/files) file (get files file-id) shared? (:is-shared file) @@ -596,24 +596,21 @@ (modal/hide!) (modal/disallow-click-outside!))) - libraries-tab - (mf/html [:> libraries-tab* - {:is-shared shared? - :linked-libraries linked-libraries - :shared-libraries shared-libraries}]) + selected-tab* + (mf/use-state #(d/nilv starting-tab "libraries")) - updates-tab - (mf/html [:> updates-tab* - {:file-id file-id - :libraries linked-libraries}]) + selected-tab + (deref selected-tab*) + + on-change-tab + (mf/use-fn #(reset! selected-tab* %)) tabs - #js [#js {:label (tr "workspace.libraries.libraries") - :id "libraries" - :content libraries-tab} - #js {:label (tr "workspace.libraries.updates") - :id "updates" - :content updates-tab}]] + (mf/with-memo [] + [{:label (tr "workspace.libraries.libraries") + :id "libraries"} + {:label (tr "workspace.libraries.updates") + :id "updates"}])] (mf/with-effect [] (st/emit! (dtm/fetch-shared-files))) @@ -631,7 +628,19 @@ (tr "workspace.libraries.libraries")] [:> tab-switcher* {:tabs tabs - :default-selected (dm/str starting-tab)}]]])) + :selected selected-tab + :on-change on-change-tab} + (case selected-tab + "libraries" + [:> libraries-tab* + {:is-shared shared? + :linked-libraries linked-libraries + :shared-libraries shared-libraries}] + + "updates" + [:> updates-tab* + {:file-id file-id + :libraries linked-libraries}])]]])) (mf/defc v2-info-dialog {::mf/register modal/components diff --git a/frontend/src/app/main/ui/workspace/sidebar.cljs b/frontend/src/app/main/ui/workspace/sidebar.cljs index 564e8aa7df..11ce398f89 100644 --- a/frontend/src/app/main/ui/workspace/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar.cljs @@ -19,18 +19,19 @@ [app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]] + [app.main.ui.hooks :as hooks] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.workspace.comments :refer [comments-sidebar*]] - [app.main.ui.workspace.left-header :refer [left-header]] + [app.main.ui.workspace.left-header :refer [left-header*]] [app.main.ui.workspace.right-header :refer [right-header*]] - [app.main.ui.workspace.sidebar.assets :refer [assets-toolbox]] - [app.main.ui.workspace.sidebar.debug :refer [debug-panel]] - [app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info]] + [app.main.ui.workspace.sidebar.assets :refer [assets-toolbox*]] + [app.main.ui.workspace.sidebar.debug :refer [debug-panel*]] + [app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info*]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox*]] - [app.main.ui.workspace.sidebar.layers :refer [layers-toolbox]] + [app.main.ui.workspace.sidebar.layers :refer [layers-toolbox*]] [app.main.ui.workspace.sidebar.options :refer [options-toolbox*]] - [app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]] - [app.main.ui.workspace.sidebar.sitemap :refer [sitemap]] + [app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container*]] + [app.main.ui.workspace.sidebar.sitemap :refer [sitemap*]] [app.main.ui.workspace.sidebar.versions :refer [versions-toolbox*]] [app.main.ui.workspace.tokens.sidebar :refer [tokens-sidebar-tab*]] [app.util.debug :as dbg] @@ -40,20 +41,61 @@ ;; --- Left Sidebar (Component) -(mf/defc collapse-button - {::mf/wrap [mf/memo] - ::mf/wrap-props false} - [{:keys [on-click] :as props}] +(defn- on-collapse-left-sidebar + [] + (st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) + +(mf/defc collapse-button* + [] ;; NOTE: This custom button may be replace by an action button when this variant is designed [:button {:class (stl/css :collapse-sidebar-button) - :on-click on-click} + :on-click on-collapse-left-sidebar} [:> icon* {:icon-id "arrow" :size "s" :aria-label (tr "workspace.sidebar.collapse")}]]) +(mf/defc layers-content* + {::mf/private true + ::mf/memo true} + [{:keys [width layout]}] + (let [{on-pointer-down :on-pointer-down + on-lost-pointer-capture :on-lost-pointer-capture + on-pointer-move :on-pointer-move + height :size} + (use-resize-hook :sitemap 200 38 "0.6" :y false nil) + + sitemap-collapsed* + (hooks/use-persisted-state ::sitemap-collapsed false) + + sitemap-collapsed? + (deref sitemap-collapsed*) + + on-toggle-sitemap-collapsed + (mf/use-fn #(reset! sitemap-collapsed* not)) + + sitemap-height + (if sitemap-collapsed? 32 height)] + + [:article {:class (stl/css :layers-tab) + :style {:--height (dm/str height "px")}} + + [:> sitemap* {:layout layout + :height sitemap-height + :collapsed sitemap-collapsed? + :on-toggle-collapsed on-toggle-sitemap-collapsed}] + + (when-not ^boolean sitemap-collapsed? + [:div {:class (stl/css :resize-area-horiz) + :on-pointer-down on-pointer-down + :on-lost-pointer-capture on-lost-pointer-capture + :on-pointer-move on-pointer-move} + + [:div {:class (stl/css :resize-handle-horiz)}]]) + + [:> layers-toolbox* {:size-parent width}]])) + (mf/defc left-sidebar* - {::mf/wrap [mf/memo] - ::mf/props :obj} + {::mf/memo true} [{:keys [layout file page-id] :as props}] (let [options-mode (mf/deref refs/options-mode-global) project (mf/deref refs/project) @@ -73,93 +115,53 @@ on-lost-pointer-capture :on-lost-pointer-capture on-pointer-move :on-pointer-move parent-ref :parent-ref - size :size} + width :size} (use-resize-hook :left-sidebar 318 318 500 :x false :left) - {on-pointer-down-pages :on-pointer-down - on-lost-pointer-capture-pages :on-lost-pointer-capture - on-pointer-move-pages :on-pointer-move - size-pages-opened :size} - (use-resize-hook :sitemap 200 38 "0.6" :y false nil) - - show-pages? (mf/use-state true) - toggle-pages (mf/use-fn #(reset! show-pages? not)) - size-pages (mf/with-memo [show-pages? size-pages-opened] - (if @show-pages? size-pages-opened 32)) - - handle-collapse - (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar))) - on-tab-change (mf/use-fn (fn [id] + (st/emit! (dcm/go-to-workspace :layout (keyword id))) (when (= id "tokens") - (st/emit! (ptk/event ::ev/event {::ev/name "open-tokens-tab"}))) - (st/emit! (dcm/go-to-workspace :layout (keyword id))))) - - layers-tab - (mf/html - [:article {:class (stl/css :layers-tab) - :style #js {"--height" (str size-pages "px")}} - - [:& sitemap {:layout layout - :toggle-pages toggle-pages - :show-pages? @show-pages? - :size size-pages}] - - (when @show-pages? - [:div {:class (stl/css :resize-area-horiz) - :on-pointer-down on-pointer-down-pages - :on-lost-pointer-capture on-lost-pointer-capture-pages - :on-pointer-move on-pointer-move-pages} - - [:div {:class (stl/css :resize-handle-horiz)}]]) - - [:& layers-toolbox {:size-parent size - :size size-pages}]]) - - - assets-tab - (mf/html [:& assets-toolbox {:size (- size 58) :file-id file-id}]) - - tokens-tab - (when design-tokens? - (mf/html [:> tokens-sidebar-tab*])) + (st/emit! (ptk/event ::ev/event {::ev/name "open-tokens-tab"}))))) tabs - (if ^boolean mode-inspect? - #js [#js {:label (tr "workspace.sidebar.layers") - :id "layers" - :content layers-tab}] - (if ^boolean design-tokens? - #js [#js {:label (tr "workspace.sidebar.layers") - :id "layers" - :content layers-tab} - #js {:label (tr "workspace.toolbar.assets") - :id "assets" - :content assets-tab} - #js {:label "Tokens" - :id "tokens" - :content tokens-tab}] - #js [#js {:label (tr "workspace.sidebar.layers") - :id "layers" - :content layers-tab} - #js {:label (tr "workspace.toolbar.assets") - :id "assets" - :content assets-tab}]))] + (mf/with-memo [mode-inspect? design-tokens?] + (if ^boolean mode-inspect? + [{:label (tr "workspace.sidebar.layers") + :id "layers"}] + (if ^boolean design-tokens? + [{:label (tr "workspace.sidebar.layers") + :id "layers"} + {:label (tr "workspace.toolbar.assets") + :id "assets"} + {:label "Tokens" + :id "tokens"}] + [{:label (tr "workspace.sidebar.layers") + :id "layers"} + {:label (tr "workspace.toolbar.assets") + :id "assets"}]))) - [:& (mf/provider muc/sidebar) {:value :left} + aside-class + (stl/css-case + :left-settings-bar true + :global/two-row (<= width 300) + :global/three-row (and (> width 300) (<= width 400)) + :global/four-row (> width 400)) + + tabs-action-button + (mf/with-memo [] + (mf/html [:> collapse-button* {}]))] + + [:> (mf/provider muc/sidebar) {:value :left} [:aside {:ref parent-ref :id "left-sidebar-aside" :data-testid "left-sidebar" - :data-size (str size) - :class (stl/css-case :left-settings-bar true - :global/two-row (<= size 300) - :global/three-row (and (> size 300) (<= size 400)) - :global/four-row (> size 400)) - :style #js {"--width" (dm/str size "px")}} + :data-size (str width) + :class aside-class + :style {:--width (dm/str width "px")}} - [:& left-header + [:> left-header* {:file file :layout layout :project project @@ -170,31 +172,92 @@ :on-lost-pointer-capture on-lost-pointer-capture :on-pointer-move on-pointer-move :class (stl/css :resize-area)}] + (cond (true? shortcuts?) - [:& shortcuts-container {:class (stl/css :settings-bar-content)}] + [:> shortcuts-container* {:class (stl/css :settings-bar-content)}] (true? show-debug?) - [:& debug-panel {:class (stl/css :settings-bar-content)}] + [:> debug-panel* {:class (stl/css :settings-bar-content)}] :else [:div {:class (stl/css :settings-bar-content)} [:> tab-switcher* {:tabs tabs - :default-selected "layers" + :default "layers" :selected (name section) - :on-change-tab on-tab-change + :on-change on-tab-change :class (stl/css :left-sidebar-tabs) :action-button-position "start" - :action-button (mf/html [:& collapse-button {:on-click handle-collapse}])}]])]])) + :action-button tabs-action-button} + + (case section + :assets + [:> assets-toolbox* + {:size (- width 58) + :file-id file-id}] + + :tokens + [:> tokens-sidebar-tab*] + + :layers + [:> layers-content* + {:layout layout + :width width}])]])]])) ;; --- Right Sidebar (Component) -(mf/defc right-sidebar* - {::mf/wrap [mf/memo]} - [{:keys [layout section file page-id] :as props}] - (let [drawing-tool (:tool (mf/deref refs/workspace-drawing)) +(defn- on-close-document-history + [] + (st/emit! (dw/remove-layout-flag :document-history))) - is-comments? (= drawing-tool :comments) +(mf/defc history-content* + {::mf/private true + ::mf/memo true} + [] + (let [selected* + (hooks/use-persisted-state ::history-sidebar "history") + + selected + (deref selected*) + + on-change-tab + (mf/use-fn #(reset! selected* %)) + + tabs + (mf/with-memo [] + [{:label (tr "workspace.versions.tab.history") + :id "history"} + {:label (tr "workspace.versions.tab.actions") + :id "actions"}]) + + button + (mf/with-memo [] + (mf/html + [:> icon-button* {:variant "ghost" + :aria-label (tr "labels.close") + :on-click on-close-document-history + :icon "close"}]))] + + [:> tab-switcher* {:tabs tabs + :selected selected + :on-change on-change-tab + :class (stl/css :left-sidebar-tabs) + :action-button-position "end" + :action-button button} + + (case selected + "history" + [:article {:class (stl/css :history-tab)} + [:> versions-toolbox* {}]] + + "actions" + [:article {:class (stl/css :versions-tab)} + [:> history-toolbox*]])])) + +(mf/defc right-sidebar* + {::mf/memo true} + [{:keys [layout section file page-id drawing-tool] :as props}] + (let [is-comments? (= drawing-tool :comments) is-history? (contains? layout :document-history) is-inspect? (= section :inspect) @@ -210,38 +273,36 @@ is-inspect? (= current-section :code))) - {:keys [on-pointer-down on-lost-pointer-capture on-pointer-move set-size size]} + {on-pointer-down :on-pointer-down + on-lost-pointer-capture :on-lost-pointer-capture + on-pointer-move :on-pointer-move + set-width :set-size + width :size} (use-resize-hook :code sidebar-default-width sidebar-default-width sidebar-default-max-width :x true :right) on-change-section - (mf/use-fn - (fn [section] - (reset! current-section* section))) - - on-close-history - (mf/use-fn #(st/emit! (dw/remove-layout-flag :document-history))) + (mf/use-fn #(reset! current-section* %)) on-expand (mf/use-fn - (mf/deps size) + (mf/deps width set-width) (fn [] - (set-size (if (> size sidebar-default-width) sidebar-default-width sidebar-default-max-width)))) - - props - (mf/spread-props props - {:on-change-section on-change-section - :on-expand on-expand})] + (set-width (if (> width sidebar-default-width) + sidebar-default-width + sidebar-default-max-width))))] [:> (mf/provider muc/sidebar) {:value :right} [:aside {:class (stl/css-case :right-settings-bar true :not-expand (not can-be-expanded?) - :expanded (> size sidebar-default-width)) + :expanded (> width sidebar-default-width)) :id "right-sidebar-aside" :data-testid "right-sidebar" - :data-size (str size) - :style {"--width" (if can-be-expanded? (dm/str size "px") (dm/str sidebar-default-width "px"))}} + :data-size (str width) + :style {:--width (if can-be-expanded? + (dm/str width "px") + (dm/str sidebar-default-width "px"))}} (when can-be-expanded? [:div {:class (stl/css :resize-area) @@ -257,42 +318,16 @@ [:div {:class (stl/css :settings-bar-inside)} (cond dbg-shape-panel? - [:& debug-shape-info] + [:> debug-shape-info*] is-comments? [:> comments-sidebar* {}] is-history? - (let [history-tab - (mf/html - [:article {:class (stl/css :history-tab)} - [:> history-toolbox*]]) - - versions-tab - (mf/html - [:article {:class (stl/css :versions-tab)} - [:> versions-toolbox*]]) - - button - (mf/html - [:> icon-button* {:variant "ghost" - :aria-label (tr "labels.close") - :on-click on-close-history - :icon "close"}]) - tabs (mf/object - [{:label (tr "workspace.versions.tab.history") - :id "history" - :content versions-tab} - {:label (tr "workspace.versions.tab.actions") - :id "actions" - :content history-tab}])] - - [:> tab-switcher* - {:tabs tabs - :default-selected "history" - :class (stl/css :left-sidebar-tabs) - :action-button-position "end" - :action-button button}]) + [:> history-content* {}] :else - [:> options-toolbox* props])]]])) + (let [props (mf/spread-props props + {:on-change-section on-change-section + :on-expand on-expand})] + [:> options-toolbox* props]))]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 466715292b..9237b5acea 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -71,9 +71,8 @@ [v [a b]] (if (= v a) b a)) -(mf/defc assets-toolbox - {::mf/wrap [mf/memo] - ::mf/wrap-props false} +(mf/defc assets-toolbox* + {::mf/wrap [mf/memo]} [{:keys [size file-id]}] (let [read-only? (mf/use-ctx ctx/workspace-read-only?) filters* (mf/use-state diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs index 4c6a715b23..27c3fcb7df 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/debug.cljs @@ -18,8 +18,8 @@ [app.util.i18n :as i18n :refer [tr]] [rumext.v2 :as mf])) -(mf/defc debug-panel - [{:keys [class] :as props}] +(mf/defc debug-panel* + [{:keys [class]}] (let [on-toggle-enabled (mf/use-fn (fn [event option] diff --git a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs index 12745448f5..b82adabff3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/debug_shape_info.cljs @@ -118,9 +118,9 @@ [:div {:class (stl/css :attrs-container-value)} (str value)])) -(mf/defc debug-shape-info +(mf/defc debug-shape-info* [] - (let [objects (mf/deref refs/workspace-page-objects) + (let [objects (mf/deref refs/workspace-page-objects) selected (->> (mf/deref refs/selected-shapes) (map (d/getf objects)))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index a1a88c05dd..eb4537dd10 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -475,9 +475,8 @@ (dom/add-class! last-hidden-frame "sticky")))))) -(mf/defc layers-toolbox - {::mf/wrap [mf/memo] - ::mf/wrap-props false} +(mf/defc layers-toolbox* + {::mf/wrap [mf/memo]} [{:keys [size-parent]}] (let [page (mf/deref refs/workspace-page) focus (mf/deref refs/workspace-focus-selected) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index dfe86f6a8d..1ebc0fd53a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -134,6 +134,7 @@ :file-id file-id :libraries libraries}])])) +;; FIXME: need optimizations (mf/defc options-content* {::mf/memo true ::mf/private true} @@ -155,59 +156,47 @@ (st/emit! :interrupt (dwc/set-workspace-read-only true)) (st/emit! :interrupt (dwc/set-workspace-read-only false))))) - design-content - (mf/html [:> design-menu* - {:selected selected - :objects objects - :page-id page-id - :file-id file-id - :selected-shapes selected-shapes - :shapes-with-children shapes-with-children}]) - - inspect-content - (mf/html [:div {:class (stl/css :element-options :inspect-options)} - [:& hrs/right-sidebar {:page-id page-id - :objects objects - :file-id file-id - :frame shape-parent-frame - :shapes selected-shapes - :on-change-section on-change-section - :on-expand on-expand - :from :workspace}]]) - - interactions-content - (mf/html [:div {:class (stl/css :element-options :interaction-options)} - [:& interactions-menu {:shape (first shapes)}]]) - - tabs - (if (:can-edit permissions) - #js [#js {:label (tr "workspace.options.design") - :id "design" - :content design-content} - - #js {:label (tr "workspace.options.prototype") - :id "prototype" - :content interactions-content} - - #js {:label (tr "workspace.options.inspect") - :id "inspect" - :content inspect-content}] - #js [#js {:label (tr "workspace.options.inspect") - :id "inspect" - :content inspect-content}])] + (mf/with-memo [] + [{:label (tr "workspace.options.design") + :id "design"} + {:label (tr "workspace.options.prototype") + :id "prototype"} + {:label (tr "workspace.options.inspect") + :id "inspect"}])] [:div {:class (stl/css :tool-window)} (if (:can-edit permissions) [:> tab-switcher* {:tabs tabs - :default-selected "info" - :on-change-tab on-change-tab + :on-change on-change-tab :selected (name options-mode) - :class (stl/css :options-tab-switcher)}] + :class (stl/css :options-tab-switcher)} + (case options-mode + :prototype + [:div {:class (stl/css :element-options :interaction-options)} + [:& interactions-menu {:shape (first shapes)}]] - [:div {:class (stl/css-case :element-options true - :inspect-options true - :read-only true)} + :inspect + [:div {:class (stl/css :element-options :inspect-options)} + [:& hrs/right-sidebar {:page-id page-id + :objects objects + :file-id file-id + :frame shape-parent-frame + :shapes selected-shapes + :on-change-section on-change-section + :on-expand on-expand + :from :workspace}]] + + :design + [:> design-menu* {:selected selected + :objects objects + :page-id page-id + :file-id file-id + :selected-shapes selected-shapes + :shapes-with-children shapes-with-children}])] + + ;; FIXME: Reuse tab??? + [:div {:class (stl/css :element-options :inspect-options :read-only)} [:& hrs/right-sidebar {:page-id page-id :objects objects :file-id file-id diff --git a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs index 093ee37219..23a980b5e9 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/shortcuts.cljs @@ -364,8 +364,8 @@ :match-section? match-section? :filter-term filter-term}]]]))) -(mf/defc shortcuts-container - [{:keys [class] :as props}] +(mf/defc shortcuts-container* + [{:keys [class]}] (let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 99c2886ae8..49ddedff3d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -193,8 +193,8 @@ ;; --- Pages List -(mf/defc pages-list - {::mf/wrap-props false} +(mf/defc pages-list* + {::mf/private true} [{:keys [file]}] (let [pages (:pages file) deletable? (> (count pages) 1) @@ -213,9 +213,8 @@ ;; --- Sitemap Toolbox -(mf/defc sitemap - {::mf/wrap-props false} - [{:keys [size show-pages? toggle-pages]}] +(mf/defc sitemap* + [{:keys [height collapsed on-toggle-collapsed]}] (let [file (mf/deref ref:file-with-pages) file-id (get file :id) project-id (get file :project-id) @@ -230,11 +229,11 @@ permissions (mf/use-ctx ctx/permissions)] [:div {:class (stl/css :sitemap) - :style #js {"--height" (str size "px")}} + :style {:--height (dm/str height "px")}} [:& title-bar {:collapsable true - :collapsed (not show-pages?) - :on-collapsed toggle-pages + :collapsed collapsed + :on-collapsed on-toggle-collapsed :all-clickable true :title (tr "workspace.sidebar.sitemap") :class (stl/css :title-spacing-sitemap)} @@ -250,5 +249,7 @@ :on-click on-create :icon "add"}])] - [:div {:class (stl/css :tool-window-content)} - [:& pages-list {:file file :key (:id file)}]]])) + (when-not ^boolean collapsed + [:div {:class (stl/css :tool-window-content)} + [:> pages-list* {:file file :key (dm/str (:id file))}]])])) + diff --git a/frontend/src/app/main/ui/workspace/tokens/export/modal.cljs b/frontend/src/app/main/ui/workspace/tokens/export/modal.cljs index 40dd59ae68..432c91f568 100644 --- a/frontend/src/app/main/ui/workspace/tokens/export/modal.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/export/modal.cljs @@ -105,31 +105,31 @@ (mf/defc export-modal-body* {::mf/private true} [] - (let [selected-tab (mf/use-state "single-file") + (let [selected-tab* (mf/use-state "single") + selected-tab (deref selected-tab*) on-change-tab - (mf/use-fn - (fn [tab-id] - (reset! selected-tab tab-id))) + (mf/use-fn #(reset! selected-tab* %)) - single-file-content - (mf/html [:> single-file-tab*]) - - multiple-files-content - (mf/html [:> multi-file-tab*]) - - tabs #js [#js {:label (tr "workspace.tokens.export.single-file") - :id "single-file" - :content single-file-content} - #js {:label (tr "workspace.tokens.export.multiple-files") - :id "multiple-files" - :content multiple-files-content}]] + tabs + (mf/with-memo [] + [{:label (tr "workspace.tokens.export.single-file") + :id "single"} + {:label (tr "workspace.tokens.export.multiple-files") + :id "multiple"}])] [:div {:class (stl/css :export-modal-wrapper)} - [:> heading* {:level 2 :typography "headline-medium" :class (stl/css :export-modal-title)} + [:> heading* {:level 2 + :typography "headline-medium" + :class (stl/css :export-modal-title)} (tr "workspace.tokens.export-tokens")] - [:> tab-switcher* - {:tabs tabs - :selected @selected-tab - :on-change-tab on-change-tab}]])) + [:> tab-switcher* {:tabs tabs + :selected selected-tab + :on-change on-change-tab} + (case selected-tab + "single" + [:> single-file-tab* {}] + + "multiple" + [:> multi-file-tab* {}])]]))