0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-16 00:41:25 -05:00

Merge pull request from penpot/eva-replace-tabs-component

♻️ Replace tabs component
This commit is contained in:
Andrey Antukh 2024-08-22 10:31:22 +02:00 committed by GitHub
commit 4765685440
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 512 additions and 729 deletions

View file

@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.12"
:git/sha "ab819f5"
{:git/tag "v2.13"
:git/sha "dc8e1e5"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}

View file

@ -10,7 +10,6 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.debug.components-preview :as cm]
[app.main.ui.debug.icons-preview :refer [icons-preview]]
[app.main.ui.frame-preview :as frame-preview]
[app.main.ui.icons :as i]
@ -169,12 +168,6 @@
:layout-name layout
:key file-id}]])
:debug-components-preview
[:div.debug-preview
[:h1 "Components preview"]
[:& cm/components-preview]]
:frame-preview
[:& frame-preview/frame-preview]

View file

@ -1,270 +0,0 @@
;; 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
(ns app.main.ui.debug.components-preview
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.main.data.users :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(mf/defc component-wrapper
{::mf/wrap-props false}
[props]
(let [children (unchecked-get props "children")
title (unchecked-get props "title")]
[:div {:class (stl/css :component)}
[:h4 {:class (stl/css :component-name)} title]
children]))
(mf/defc components-preview
{::mf/wrap-props false}
[]
(let [profile (mf/deref refs/profile)
initial (mf/with-memo [profile]
(update profile :lang #(or % "")))
initial-theme (:theme initial)
on-change (fn [event]
(let [theme (dom/event->value event)
data (assoc initial :theme theme)]
(st/emit! (du/update-profile data))))
colors ["var(--color-background-primary)"
"var(--color-background-secondary)"
"var(--color-background-tertiary)"
"var(--color-background-quaternary)"
"var(--color-foreground-primary)"
"var(--color-foreground-secondary)"
"var(--color-accent-primary)"
"var(--color-accent-primary-muted)"
"var(--color-accent-secondary)"
"var(--color-accent-tertiary)"]
;; COMPONENTS FNs
state* (mf/use-state {:collapsed? true
:tab-selected :first
:input-value ""
:radio-selected "first"})
state (deref state*)
collapsed? (:collapsed? state)
toggle-collapsed
(mf/use-fn #(swap! state* update :collapsed? not))
tab-selected (:tab-selected state)
set-tab (mf/use-fn #(swap! state* assoc :tab-selected %))
input-value (:input-value state)
radio-selected (:radio-selected state)
set-radio-selected (mf/use-fn #(swap! state* assoc :radio-selected %))
update-search
(mf/use-fn
(fn [value _event]
(swap! state* assoc :input-value value)))
on-btn-click (mf/use-fn #(prn "eyy"))]
[:section.debug-components-preview
[:div {:class (stl/css :themes-row)}
[:h2 "Themes"]
[:select {:label "Select theme color"
:name :theme
:default "default"
:value initial-theme
:on-change on-change}
[:option {:label "Penpot Dark (default)" :value "default"}]
[:option {:label "Penpot Light" :value "light"}]]
[:div {:class (stl/css :wrapper)}
(for [color colors]
[:div {:class (stl/css :color-wrapper)}
[:span (d/name color)]
[:div {:key color
:style {:background color}
:class (stl/css :rect)}]])]]
[:div {:class (stl/css :components-row)}
[:h2 {:class (stl/css :title)} "Components"]
[:div {:class (stl/css :components-wrapper)}
[:div {:class (stl/css :components-group)}
[:h3 "Titles"]
[:& component-wrapper
{:title "Title"}
[:& title-bar {:collapsable false
:title "Title"}]]
[:& component-wrapper
{:title "Title and action button"}
[:& title-bar {:collapsable false
:title "Title"
:on-btn-click on-btn-click
:btn-children i/add}]]
[:& component-wrapper
{:title "Collapsed title and action button"}
[:& title-bar {:collapsable true
:collapsed collapsed?
:on-collapsed toggle-collapsed
:title "Title"
:on-btn-click on-btn-click
:btn-children i/add}]]
[:& component-wrapper
{:title "Collapsed title and children"}
[:& title-bar {:collapsable true
:collapsed collapsed?
:on-collapsed toggle-collapsed
:title "Title"}
[:& tab-container {:on-change-tab set-tab
:selected tab-selected}
[:& tab-element {:id :first
:title "A tab"}]
[:& tab-element {:id :second
:title "B tab"}]]]]]
[:div {:class (stl/css :components-group)}
[:h3 "Tabs component"]
[:& component-wrapper
{:title "2 tab component"}
[:& tab-container {:on-change-tab set-tab
:selected tab-selected}
[:& tab-element {:id :first :title "First tab"}
[:div "This is first tab content"]]
[:& tab-element {:id :second :title "Second tab"}
[:div "This is second tab content"]]]]
[:& component-wrapper
{:title "3 tab component"}
[:& tab-container {:on-change-tab set-tab
:selected tab-selected}
[:& tab-element {:id :first :title "First tab"}
[:div "This is first tab content"]]
[:& tab-element {:id :second
:title "Second tab"}
[:div "This is second tab content"]]
[:& tab-element {:id :third
:title "Third tab"}
[:div "This is third tab content"]]]]]
[:div {:class (stl/css :components-group)}
[:h3 "Search bar"]
[:& component-wrapper
{:title "Search bar only"}
[:& search-bar {:on-change update-search
:value input-value
:placeholder "Test value"}]]
[:& component-wrapper
{:title "Search and button"}
[:& search-bar {:on-change update-search
:value input-value
:placeholder "Test value"}
[:button {:class (stl/css :button-secondary)
:on-click on-btn-click}
"X"]]]]
[:div {:class (stl/css :components-group)}
[:h3 "Radio buttons"]
[:& component-wrapper
{:title "Two radio buttons (toggle)"}
[:& radio-buttons {:selected radio-selected
:on-change set-radio-selected
:name "listing-style"}
[:& radio-button {:icon i/view-as-list
:value "first"
:id :list}]
[:& radio-button {:icon i/flex-grid
:value "second"
:id :grid}]]]
[:& component-wrapper
{:title "Three radio buttons"}
[:& radio-buttons {:selected radio-selected
:on-change set-radio-selected
:name "listing-style"}
[:& radio-button {:icon i/view-as-list
:value "first"
:id :first}]
[:& radio-button {:icon i/flex-grid
:value "second"
:id :second}]
[:& radio-button {:icon i/add
:value "third"
:id :third}]]]
[:& component-wrapper
{:title "Four radio buttons"}
[:& radio-buttons {:selected radio-selected
:on-change set-radio-selected
:name "listing-style"}
[:& radio-button {:icon i/view-as-list
:value "first"
:id :first}]
[:& radio-button {:icon i/flex-grid
:value "second"
:id :second}]
[:& radio-button {:icon i/add
:value "third"
:id :third}]
[:& radio-button {:icon i/board
:value "forth"
:id :forth}]]]]
[:div {:class (stl/css :components-group)}
[:h3 "Buttons"]
[:& component-wrapper
{:title "Button primary"}
[:button {:class (stl/css :button-primary)}
"Primary"]]
[:& component-wrapper
{:title "Button primary with icon"}
[:button {:class (stl/css :button-primary)}
i/add]]
[:& component-wrapper
{:title "Button secondary"}
[:button {:class (stl/css :button-secondary)}
"secondary"]]
[:& component-wrapper
{:title "Button secondary with icon"}
[:button {:class (stl/css :button-secondary)}
i/add]]
[:& component-wrapper
{:title "Button tertiary"}
[:button {:class (stl/css :button-tertiary)}
"tertiary"]]
[:& component-wrapper
{:title "Button tertiary with icon"}
[:button {:class (stl/css :button-tertiary)}
i/add]]]
[:div {:class (stl/css :components-group)}
[:h3 "Inputs"]
[:& component-wrapper
{:title "Only input"}
[:div {:class (stl/css :input-wrapper)}
[:input {:class (stl/css :basic-input)
:placeholder "----"}]]]
[:& component-wrapper
{:title "Input with label"}
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)} "label"]
[:input {:class (stl/css :basic-input)
:placeholder "----"}]]]
[:& component-wrapper
{:title "Input with icon"}
[:div {:class (stl/css :input-wrapper)}
[:span {:class (stl/css :input-label)}
i/add]
[:input {:class (stl/css :basic-input)
:placeholder "----"}]]]]]]]))

View file

@ -1,99 +0,0 @@
// 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
@import "refactor/common-refactor.scss";
.themes-row {
width: 100%;
padding: $s-20;
color: var(--color-foreground-primary);
background: var(--color-background-secondary);
.wrapper {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $s-40;
background-color: var(--color-background-primary);
width: 100%;
padding: $s-20;
.rect {
display: flex;
justify-content: center;
align-items: center;
border: $s-1 solid var(--color-foreground-primary);
padding: $s-20;
height: $s-96;
min-width: $s-152;
}
}
}
.color-wrapper {
display: grid;
grid-template-rows: auto $s-96;
}
.components-row {
color: var(--color-foreground-primary);
background: var(--color-background-secondary);
height: 100%;
padding: 0 $s-20;
.title {
padding: $s-20;
}
.components-wrapper {
padding: $s-20;
display: flex;
flex-wrap: wrap;
gap: $s-20;
.components-group {
@include flexCenter;
justify-content: flex-start;
flex-direction: column;
border-radius: $s-8;
h3 {
@include bodySmallTypography;
font-size: $fs-24;
width: 100%;
}
.component {
display: flex;
flex-direction: column;
gap: $s-8;
width: $s-240;
max-height: $s-80;
margin-bottom: $s-16;
.component-name {
@include uppercaseTitleTipography;
font-weight: bold;
}
}
}
.button-primary {
@extend .button-primary;
height: $s-32;
svg {
@extend .button-icon;
}
}
.button-secondary {
@extend .button-secondary;
height: $s-32;
svg {
@extend .button-icon;
}
}
.button-tertiary {
@extend .button-tertiary;
height: $s-32;
svg {
@extend .button-icon;
}
}
.input-wrapper {
@extend .input-element;
@include bodySmallTypography;
}
}
}

View file

@ -9,8 +9,8 @@
[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] :as i]
[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]
@ -19,45 +19,50 @@
(mf/defc tab*
{::mf/props :obj
::mf/private true}
[{:keys [selected icon label aria-label id tab-ref] :rest props}]
[{:keys [selected icon label aria-label id on-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 tab-ref
:data-id id})]
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))
:data-id id
;; This prop is to be used for accessibility purposes only.
:id id})]
[:> "li" {}
[:> "button" props
(when icon
[:li
[:> :button props
(when (some? icon)
[:> icon*
{:id icon
:aria-hidden (when label true)
:aria-label (when (not label) aria-label)}])
(when label
(when (string? label)
[:span {:class (stl/css-case :tab-text true
:tab-text-and-icon icon)} label])]]))
:tab-text-and-icon icon)}
label])]]))
(mf/defc tab-nav*
{::mf/props :obj
::mf/private true}
[{:keys [tabs-refs tabs selected on-click button-position action-button] :rest props}]
[{: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}
props (mf/spread-props props
{:class (stl/css :tab-list)
:role "tablist"
:aria-orientation "horizontal"})]
[:nav {:class class}
(when (= button-position "start")
action-button)
[:> "ul" props
(for [[index element] (map-indexed vector tabs)]
(for [element ^js tabs]
(let [icon (obj/get element "icon")
label (obj/get element "label")
aria-label (obj/get element "aria-label")
@ -67,94 +72,129 @@
:key (dm/str "tab-" id)
:label label
:aria-label aria-label
:selected (= index selected)
:selected (= id selected)
:on-click on-click
:id id
:tab-ref (nth tabs-refs index)}]))]
:on-ref on-ref
:id id}]))]
(when (= button-position "end")
action-button)]))
(mf/defc tab-panel*
{::mf/props :obj
::mf/private true}
[{:keys [children name] :rest props}]
(let [props (mf/spread-props props {:class (stl/css :tab-panel)
:aria-labelledby name
:role "tabpanel"})]
[:> "section" props
children]))
(defn- get-tab
[tabs id]
(or (array/find #(= id (obj/get % "id")) tabs)
(aget tabs 0)))
(defn- valid-tabs?
[tabs]
(every? (fn [tab]
(let [icon (obj/get tab "icon")
label (obj/get tab "label")
aria-label (obj/get tab "aria-label")]
(and (or (not icon) (contains? icon-list icon))
(not (and icon (nil? label) (nil? aria-label)))
(not (and aria-label (or (nil? icon) label))))))
(seq tabs)))
(defn- get-selected-tab-id
[tabs default]
(let [tab (get-tab tabs default)]
(obj/get tab "id")))
(def ^:private positions (set '("start" "end")))
(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?]]
[:fn {:error/message "invalid data: missing required props"}
(fn [tab]
(or (and (contains? tab :icon)
(or (contains? tab :label)
(contains? tab :aria-label)))
(contains? tab :label)))]])
(defn- valid-button-position? [position button]
(or (nil? position) (and (contains? positions position) (some? button))))
(def ^:private schema:tab-switcher
[:map
[:class {:optional true} :string]
[:action-button-position {:optional true}
[:enum "start" "end"]]
[:default-selected {:optional true} :string]
[:tabs [:vector {:min 1} schema:tab]]])
(mf/defc tab-switcher*
{::mf/props :obj}
{::mf/props :obj
::mf/schema schema:tab-switcher}
[{:keys [class tabs on-change-tab default-selected action-button-position action-button] :rest props}]
;; TODO: Use a schema to assert the tabs prop -> https://tree.taiga.io/project/penpot/task/8521
(assert (valid-tabs? tabs) "unexpected props for tab-switcher")
(assert (valid-button-position? action-button-position action-button) "invalid action-button-position")
(let [tab-ids (mapv #(obj/get % "id") tabs)
(let [selected* (mf/use-state #(get-selected-tab-id tabs default-selected))
selected (deref selected*)
active-tab-index* (mf/use-state (or (d/index-of tab-ids default-selected) 0))
active-tab-index (deref active-tab-index*)
tabs-nodes-refs (mf/use-ref nil)
tabs-ref (mf/use-ref nil)
tabs-refs (mapv (fn [_] (mf/use-ref)) tabs)
active-tab (nth tabs active-tab-index)
panel-content (obj/get active-tab "content")
handle-click
on-click
(mf/use-fn
(mf/deps on-change-tab tab-ids)
(mf/deps on-change-tab)
(fn [event]
(let [id (dom/get-data (dom/get-current-target event) "id")
index (d/index-of tab-ids id)]
(reset! active-tab-index* index)
(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)))))
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))))
on-key-down
(mf/use-fn
(mf/deps tabs-refs active-tab-index)
(mf/deps selected)
(fn [event]
(let [len (count tabs-refs)
index (cond
(kbd/home? event) 0
(kbd/left-arrow? event) (mod (- active-tab-index 1) len)
(kbd/right-arrow? event) (mod (+ active-tab-index 1) len))]
(when index
(reset! active-tab-index* index)
(dom/focus! (mf/ref-val (nth tabs-refs index)))))))
(let [tabs (mf/ref-val tabs-ref)
len (alength tabs)
sel? #(= selected (obj/get % "id"))
id (cond
(kbd/home? event)
(let [tab (aget tabs 0)]
(obj/get tab "id"))
(kbd/left-arrow? event)
(let [index (array/find-index sel? tabs)
index (mod (- index 1) len)
tab (aget tabs index)]
(obj/get tab "id"))
(kbd/right-arrow? event)
(let [index (array/find-index sel? tabs)
index (mod (+ index 1) len)
tab (aget tabs index)]
(obj/get tab "id")))]
(when (some? id)
(reset! selected* id)
(let [nodes (mf/ref-val tabs-nodes-refs)
node (obj/get nodes id)]
(dom/focus! node))))))
class (dm/str class " " (stl/css :tabs))
props (mf/spread-props props {:class class
:on-key-down on-key-down})]
[:> "article" props
[:> "div" {:class (stl/css :padding-wrapper)}
(mf/with-effect [tabs]
(mf/set-ref-val! tabs-ref tabs))
[:> :article props
[:div {:class (stl/css :padding-wrapper)}
[:> tab-nav* {:button-position action-button-position
:action-button action-button
:tabs tabs
:selected active-tab-index
:on-click handle-click
:tabs-refs tabs-refs}]]
[:> tab-panel* {:tab-index 0}
panel-content]]))
:on-ref on-ref
:selected selected
: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])]))

View file

@ -99,3 +99,9 @@
.tab-text-and-icon {
padding-inline: var(--sp-xxs);
}
.tab-panel {
display: grid;
width: 100%;
height: 100%;
}

View file

@ -60,8 +60,6 @@
(when *assert*
["/debug/icons-preview" :debug-icons-preview])
["/debug/components-preview" :debug-components-preview]
;; Used for export
["/render-sprite/:file-id" :render-sprite]

View file

@ -43,7 +43,7 @@
[{:keys [local file page frame index viewer-pagination size share-id]}]
(let [inspect-svg-container-ref (mf/use-ref nil)
current-section* (mf/use-state :info)
current-section (deref current-section*)
current-section (deref current-section*)
can-be-expanded? (= current-section :code)

View file

@ -41,7 +41,8 @@
content (when (= (count shapes) 1)
(ctkl/get-component-annotation (first shapes) libraries))]
[:div {:class (stl/css :element-options)}
[:div {:class (stl/css-case :element-options true
:workspace-element-options (= from :workspace))}
(for [[idx option] (map-indexed vector options)]
[:> (case option
:geometry geometry-panel

View file

@ -11,6 +11,13 @@
flex-direction: column;
gap: $s-16;
width: 100%;
height: 100%;
height: calc(100vh - #{$s-128}); // TODO: Fix this hardcoded value
padding-top: $s-8;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.workspace-element-options {
height: calc(100vh - #{$s-164}); // TODO: Fix this hardcoded value
}

View file

@ -9,9 +9,12 @@
.element-options {
display: flex;
flex-direction: column;
height: 100%;
height: calc(100vh - #{$s-128}); // TODO: Fix this hardcoded value
overflow: hidden;
padding-bottom: $s-16;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
}
.download-button {

View file

@ -11,7 +11,7 @@
[app.common.types.component :as ctk]
[app.main.refs :as refs]
[app.main.ui.components.shape-icon :as sir]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as i]
[app.main.ui.viewer.inspect.attributes :refer [attributes]]
[app.main.ui.viewer.inspect.code :refer [code]]
@ -61,9 +61,9 @@
(mf/use-fn
(mf/deps from on-change-section)
(fn [new-section]
(reset! section new-section)
(reset! section (keyword new-section))
(when on-change-section
(on-change-section new-section))))
(on-change-section (keyword new-section)))))
handle-expand
(mf/use-fn
@ -74,7 +74,33 @@
navigate-to-help
(mf/use-fn
(fn []
(dom/open-new-window "https://help.penpot.app/user-guide/inspect/")))]
(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/use-effect
(mf/deps shapes handle-change-tab)
@ -108,25 +134,11 @@
;; (tr "inspect.tabs.code.selected.text")
[:span {:class (stl/css :layer-title)} (:name first-shape)]])]
[:div {:class (stl/css :inspect-content)}
[:& tab-container {:on-change-tab handle-change-tab
:selected @section
:content-class (stl/css :tab-content)
:header-class (stl/css :tab-header)}
[:& tab-element {:id :info :title (tr "inspect.tabs.info")}
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]]
[:& tab-element {:id :code :title (tr "inspect.tabs.code")}
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}]]]]]
[:> tab-switcher* {:tabs tabs
:default-selected "info"
:on-change-tab handle-change-tab
:class (stl/css :viewer-tab-switcher)}]]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}

View file

@ -97,10 +97,6 @@
overflow: hidden;
}
.tab-content {
scrollbar-gutter: stable;
}
.tab-header {
margin-right: $s-12;
.viewer-tab-switcher {
--tabs-nav-padding-inline-end: var(--sp-m);
}

View file

@ -21,7 +21,8 @@
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.ds.foundations.assets.icon :as ic]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.colorpicker.color-inputs :refer [color-inputs]]
[app.main.ui.workspace.colorpicker.gradients :refer [gradients]]
@ -208,7 +209,50 @@
[{:value :linear-gradient :label (tr "media.linear")}
{:value :radial-gradient :label (tr "media.radial")}])
(when (not disable-image)
[{:value :image :label (tr "media.image")}])))]
[{:value :image :label (tr "media.image")}])))
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}]))}]]
;; Initialize colorpicker state
(mf/with-effect []
@ -306,46 +350,9 @@
:on-selected on-fill-image-selected}]]])
[:*
[:div {:class (stl/css :colorpicker-tabs)}
[:& tab-container
{:on-change-tab on-change-tab
:selected @active-color-tab
:collapsable false}
[:& tab-element {:id :ramp :title i/rgba}
(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}])]
[:& tab-element {:id :harmony :title i/rgba-complementary}
(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}])]
[:& tab-element {:id :hsva :title i/hsva}
(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}])]]]
[:> tab-switcher* {:tabs tabs
:default-selected "ramp"
:on-change-tab on-change-tab}]]
[:& color-inputs
{:type (if (= @active-color-tab :hsva) :hsv :rgb)

View file

@ -24,8 +24,8 @@
[app.main.ui.components.color-bullet :as cb]
[app.main.ui.components.link-button :as lb]
[app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as i]
[app.util.color :as uc]
@ -487,9 +487,6 @@
file-id (:id file)
shared? (:is-shared file)
selected-tab* (mf/use-state starting-tab)
selected-tab (deref selected-tab*)
libraries (mf/deref refs/workspace-libraries)
libraries (mf/with-memo [libraries]
(d/removem (fn [[_ val]] (:is-indirect val)) libraries))
@ -498,9 +495,6 @@
shared-libraries
(mf/deref refs/workspace-shared-files)
on-tab-change
(mf/use-fn #(reset! selected-tab* %))
close-dialog-outside
(mf/use-fn (fn [event]
(when (= (dom/get-target event) (dom/get-current-target event))
@ -509,7 +503,21 @@
close-dialog
(mf/use-fn (fn [_]
(modal/hide!)
(modal/disallow-click-outside!)))]
(modal/disallow-click-outside!)))
tabs
#js [#js {:label (tr "workspace.libraries.libraries")
:id "libraries"
:content (mf/html [:& libraries-tab {:file-id file-id
:shared? shared?
:linked-libraries libraries
:shared-libraries shared-libraries}])}
#js {:label (tr "workspace.libraries.updates")
:id "updates"
:content (mf/html [:& updates-tab {:file-id file-id
:file-data file-data
:libraries libraries}])}]]
(mf/with-effect [team-id]
(when team-id
@ -524,19 +532,9 @@
close-icon]
[:div {:class (stl/css :modal-title)}
(tr "workspace.libraries.libraries")]
[:& tab-container
{:on-change-tab on-tab-change
:selected selected-tab
:collapsable false}
[:& tab-element {:id :libraries :title (tr "workspace.libraries.libraries")}
[:& libraries-tab {:file-id file-id
:shared? shared?
:linked-libraries libraries
:shared-libraries shared-libraries}]]
[:& tab-element {:id :updates :title (tr "workspace.libraries.updates")}
[:& updates-tab {:file-id file-id
:file-data file-data
:libraries libraries}]]]]]))
[:> tab-switcher* {:tabs tabs
:default-selected (dm/str starting-tab)}]]]))
(mf/defc v2-info-dialog
{::mf/register modal/components

View file

@ -11,8 +11,9 @@
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.context :as muc]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[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]]
@ -31,6 +32,17 @@
;; --- Left Sidebar (Component)
(mf/defc collapse-button
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [on-click] :as props}]
;; 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}
[:& icon* {:id "arrow"
:size "s"
:aria-label (tr "workspace.sidebar.collapse")}]])
(mf/defc left-sidebar
{::mf/wrap [mf/memo]
::mf/wrap-props false}
@ -39,7 +51,6 @@
mode-inspect? (= options-mode :inspect)
project (mf/deref refs/workspace-project)
section (cond (or mode-inspect? (contains? layout :layers)) :layers
(contains? layout :assets) :assets)
@ -60,7 +71,49 @@
(mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
on-tab-change
(mf/use-fn #(st/emit! (dw/go-to-layout %)))]
(mf/use-fn #(st/emit! (dw/go-to-layout (keyword %))))
tabs (if ^boolean mode-inspect?
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content (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}])
[:& layers-toolbox {:size-parent size
:size size-pages}]])}]
#js [#js {:label (tr "workspace.sidebar.layers")
:id "layers"
:content (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}])
[:& layers-toolbox {:size-parent size
:size size-pages}]])}
#js {:label (tr "workspace.toolbar.assets")
:id "assets"
:content (mf/html [:& assets-toolbox {:size (- size 58)}])}])]
[:& (mf/provider muc/sidebar) {:value :left}
[:aside {:ref parent-ref
@ -89,36 +142,12 @@
:else
[:div {:class (stl/css :settings-bar-content)}
[:& tab-container
{:on-change-tab on-tab-change
:selected section
:collapsable true
:handle-collapse handle-collapse
:header-class (stl/css :tab-spacing)}
[:& tab-element {:id :layers
:title (tr "workspace.sidebar.layers")}
[: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}])
[:& layers-toolbox {:size-parent size
:size size-pages}]]]
(when-not ^boolean mode-inspect?
[:& tab-element {:id :assets
:title (tr "workspace.toolbar.assets")}
[:& assets-toolbox {:size (- size 58)}]])]])]]))
[:> tab-switcher* {:tabs tabs
:default-selected (dm/str section)
:on-change-tab on-tab-change
:class (stl/css :left-sidebar-tabs)
:action-button-position "start"
:action-button (mf/html [:& collapse-button {:on-click handle-collapse}])}]])]]))
;; --- Right Sidebar (Component)
@ -132,9 +161,6 @@
is-history? (contains? layout :document-history)
is-inspect? (= section :inspect)
;;expanded? (mf/deref refs/inspect-expanded)
;;prev-expanded? (hooks/use-previous expanded?)
current-section* (mf/use-state :info)
current-section (deref current-section*)
@ -171,7 +197,7 @@
:id "right-sidebar-aside"
:data-testid "right-sidebar"
:data-size (str size)
:style #js {"--width" (if can-be-expanded? (dm/str size "px") 276)}}
:style #js {"--width" (if can-be-expanded? (dm/str size "px") "276px")}}
(when can-be-expanded?
[:div {:class (stl/css :resize-area)
:on-pointer-down on-pointer-down

View file

@ -59,11 +59,11 @@ $width-settings-bar-max: $s-500;
.right-settings-bar {
grid-area: right-sidebar;
display: grid;
grid-template-rows: auto 1fr;
height: 100vh;
width: $width-settings-bar;
background-color: var(--panel-background-color);
height: 100%;
display: flex;
flex-direction: column;
z-index: 0;
&.not-expand {
max-width: $width-settings-bar;
@ -89,3 +89,22 @@ $width-settings-bar-max: $s-500;
border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize;
}
.left-sidebar-tabs {
--tabs-nav-padding-inline-start: var(--sp-m);
--tabs-nav-padding-inline-end: var(--sp-m);
}
.collapse-sidebar-button {
--collapse-icon-color: var(--color-foreground-secondary);
@include flexCenter;
@include buttonStyle;
height: 100%;
width: $s-24;
border-radius: $br-5;
color: var(--collapse-icon-color);
transform: rotate(180deg);
&:hover {
--collapse-icon-color: var(--color-foreground-primary);
}
}

View file

@ -9,7 +9,7 @@
(:require
[app.main.data.workspace :as dw]
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
@ -22,6 +22,8 @@
:class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button)
:on-click on-click
:aria-label (tr "workspace.sidebar.expand")}
i/arrow]]]))
:title (tr "workspace.sidebar.expand")
:on-click on-click}
[:& icon* {:id "arrow"
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]]))

View file

@ -14,10 +14,11 @@
padding: $s-4;
border-radius: $br-8;
background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
}
.collapsed-title {
@include flexCenter;
height: $s-32;
height: $s-36;
width: $s-24;
border-radius: $br-8;
background: var(--color-background-secondary);

View file

@ -15,8 +15,8 @@
[app.main.data.workspace :as udw]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.tab-container :refer [tab-container tab-element]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.tab-switcher :refer [tab-switcher*]]
[app.main.ui.viewer.inspect.right-sidebar :as hrs]
[app.main.ui.workspace.sidebar.options.menus.align :refer [align-options]]
[app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options]]
@ -72,102 +72,117 @@
(when (= (:type panel) :component-swap)
[:& component-menu {:shapes (:shapes panel) :swap-opened? true}]))
(mf/defc design-menu
{::mf/wrap [mf/memo]}
[{:keys [selected objects page-id file-id selected-shapes shapes-with-children]}]
(let [sp-panel (mf/deref refs/specialized-panel)
drawing (mf/deref refs/workspace-drawing)
shared-libs (mf/deref refs/workspace-libraries)
edition (mf/deref refs/selected-edition)
edit-grid? (ctl/grid-layout? objects edition)
grid-edition (mf/deref refs/workspace-grid-edition)
selected-cells (->> (dm/get-in grid-edition [edition :selected])
(map #(dm/get-in objects [edition :layout-grid-cells %])))]
[:div {:class (stl/css :element-options :design-options)}
[:& align-options]
[:& bool-options]
(cond
(and edit-grid? (d/not-empty? selected-cells))
[:& grid-cell/options
{:shape (get objects edition)
:cells selected-cells}]
edit-grid?
[:& layout-container/grid-layout-edition
{:ids [edition]
:values (get objects edition)}]
(not (nil? sp-panel))
[:& specialized-panel {:panel sp-panel}]
(d/not-empty? drawing)
[:& shape-options
{:shape (:object drawing)
:page-id page-id
:file-id file-id
:shared-libs shared-libs}]
(= 0 (count selected))
[:& page/options]
(= 1 (count selected))
[:& shape-options
{:shape (first selected-shapes)
:page-id page-id
:file-id file-id
:shared-libs shared-libs
:shapes-with-children shapes-with-children}]
:else
[:& multiple/options
{:shapes-with-children shapes-with-children
:shapes selected-shapes
:page-id page-id
:file-id file-id
:shared-libs shared-libs}])]))
(mf/defc options-content
{::mf/memo true
::mf/props :obj}
[{:keys [selected section shapes shapes-with-children page-id file-id on-change-section on-expand]}]
(let [drawing (mf/deref refs/workspace-drawing)
objects (mf/deref refs/workspace-page-objects)
shared-libs (mf/deref refs/workspace-libraries)
edition (mf/deref refs/selected-edition)
grid-edition (mf/deref refs/workspace-grid-edition)
sp-panel (mf/deref refs/specialized-panel)
[{:keys [selected shapes shapes-with-children page-id file-id on-change-section on-expand]}]
(let [objects (mf/deref refs/workspace-page-objects)
selected-shapes (into [] (keep (d/getf objects)) selected)
first-selected-shape (first selected-shapes)
shape-parent-frame (cfh/get-frame objects (:frame-id first-selected-shape))
edit-grid? (ctl/grid-layout? objects edition)
selected-cells (->> (dm/get-in grid-edition [edition :selected])
(map #(dm/get-in objects [edition :layout-grid-cells %])))
on-change-tab
(fn [options-mode]
(st/emit! (udw/set-options-mode options-mode))
(if (= options-mode :inspect)
(st/emit! :interrupt (udw/set-workspace-read-only true))
(st/emit! :interrupt (udw/set-workspace-read-only false))))]
(let [options-mode (keyword options-mode)]
(st/emit! (udw/set-options-mode options-mode))
(if (= options-mode :inspect)
(st/emit! :interrupt (udw/set-workspace-read-only true))
(st/emit! :interrupt (udw/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
#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}]]
[:div {:class (stl/css :tool-window)}
[:& tab-container
{:on-change-tab on-change-tab
:selected section
:collapsable false
:content-class (stl/css-case
:content-class true
:inspect (= section :inspect))
:header-class (stl/css :tab-spacing)}
[:& tab-element {:id :design
:title (tr "workspace.options.design")}
[:div {:class (stl/css :element-options)}
[:& align-options]
[:& bool-options]
(cond
(and edit-grid? (d/not-empty? selected-cells))
[:& grid-cell/options
{:shape (get objects edition)
:cells selected-cells}]
edit-grid?
[:& layout-container/grid-layout-edition
{:ids [edition]
:values (get objects edition)}]
(not (nil? sp-panel))
[:& specialized-panel {:panel sp-panel}]
(d/not-empty? drawing)
[:& shape-options
{:shape (:object drawing)
:page-id page-id
:file-id file-id
:shared-libs shared-libs}]
(= 0 (count selected))
[:& page/options]
(= 1 (count selected))
[:& shape-options
{:shape (first selected-shapes)
:page-id page-id
:file-id file-id
:shared-libs shared-libs
:shapes-with-children shapes-with-children}]
:else
[:& multiple/options
{:shapes-with-children shapes-with-children
:shapes selected-shapes
:page-id page-id
:file-id file-id
:shared-libs shared-libs}])]]
[:& tab-element {:id :prototype
:title (tr "workspace.options.prototype")}
[:div {:class (stl/css :element-options)}
[:& interactions-menu {:shape (first shapes)}]]]
[:& tab-element {:id :inspect
:title (tr "workspace.options.inspect")}
[:div {:class (stl/css :element-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}]]]]]))
[:> tab-switcher* {:tabs tabs
:default-selected "info"
:on-change-tab on-change-tab
:class (stl/css :options-tab-switcher)}]]))
;; TODO: this need optimizations, selected-objects and
;; selected-objects-with-children are derefed always but they only

View file

@ -8,8 +8,7 @@
.tool-window {
position: relative;
display: flex;
flex-direction: column;
display: grid;
width: 100%;
height: 100%;
padding-left: $s-12;
@ -27,14 +26,23 @@
}
.element-options {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
display: grid;
gap: $s-8;
width: 100%;
height: calc(100vh - $s-80);
padding-top: $s-8;
}
.design-options,
.interaction-options {
overflow: auto;
scrollbar-gutter: stable;
}
.inspect {
scrollbar-gutter: unset;
}
.options-tab-switcher {
--tabs-nav-padding-inline-end: 12px;
}

View file

@ -8,44 +8,7 @@
.stroke-data {
@include flexColumn;
.stroke-options {
@include flexRow;
.stroke-width-input-element {
@extend .input-element;
@include bodySmallTypography;
width: $s-60;
}
.select-wrapper {
width: $s-124;
}
}
.stroke-caps-options {
@include flexRow;
.cap-select {
width: $s-124;
}
.stroke-cap-dropdown,
.stroke-cap-dropdown-start {
min-width: $s-124;
width: fit-content;
max-width: $s-252;
right: 0;
left: unset;
}
.stroke-cap-dropdown-start {
left: 0;
right: unset;
}
.swap-caps-btn {
@extend .button-secondary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}
}
&.dnd-over-top {
border-top: $s-1 solid var(--layer-row-foreground-color-drag);
}
@ -53,3 +16,42 @@
border-bottom: $s-1 solid var(--layer-row-foreground-color-drag);
}
}
.stroke-options {
@include flexRow;
display: grid;
grid-template-columns: 1fr 2fr 2fr;
.stroke-width-input-element {
@extend .input-element;
@include bodySmallTypography;
}
}
.stroke-caps-options {
@include flexRow;
}
.cap-select {
width: $s-124;
}
.stroke-cap-dropdown,
.stroke-cap-dropdown-start {
min-width: $s-124;
width: fit-content;
max-width: $s-252;
right: 0;
left: unset;
}
.stroke-cap-dropdown-start {
left: 0;
right: unset;
}
.swap-caps-btn {
@extend .button-secondary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}

View file

@ -6,7 +6,9 @@
(ns app.util.array
"A collection of helpers for work with javascript arrays."
(:refer-clojure :exclude [conj! conj filter]))
(:refer-clojure :exclude [conj! conj filter map reduce find])
(:require
[cljs.core :as c]))
(defn conj
"A conj like function for js arrays."
@ -49,3 +51,19 @@
"A specific filter for js arrays."
[pred ^js/Array o]
(.filter o pred))
(defn map
[f a]
(.map ^js/Array a f))
(defn reduce
[f init val]
(.reduce ^js/Array val f init))
(defn find-index
[f v]
(.findIndex ^js/Array v f))
(defn find
[f v]
(.find ^js/Array v f))