0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-06 14:50:20 -05:00

Merge pull request #264 from tokens-studio/token-sets-themes-ui

Token sets themes UI
This commit is contained in:
Florian Schrödl 2024-08-30 11:15:31 +02:00 committed by GitHub
commit 27409f43d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 1072 additions and 121 deletions

View file

@ -102,6 +102,17 @@
(rx/of
(dch/commit-changes changes)))))))
(defn update-token-theme [token-theme]
(ptk/reify ::update-token-theme
ptk/WatchEvent
(watch [it state _]
(let [prev-token-theme (wtts/get-workspace-token-theme state (:id token-theme))
changes (-> (pcb/empty-changes it)
(pcb/update-token-theme token-theme prev-token-theme))]
(js/console.log "changes" changes)
(rx/of
(dch/commit-changes changes))))))
(defn ensure-token-theme-changes [changes state {:keys [id new-set?]}]
(let [theme-id (wtts/update-theme-id state)
theme (some-> theme-id (wtts/get-workspace-token-theme state))]
@ -155,19 +166,32 @@
(ensure-token-theme-changes state {:id (:id new-token-set)
:new-set? true}))]
(rx/of
(set-selected-token-set-id (:id new-token-set))
(dch/commit-changes changes)))))))
(defn toggle-token-set [token-set-id]
(defn update-token-set [token-set]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [prev-token-set (wtts/get-token-set (:id token-set) state)
changes (-> (pcb/empty-changes it)
(pcb/update-token-set token-set prev-token-set))]
(rx/of
(dch/commit-changes changes))))))
(defn toggle-token-set [{:keys [token-set-id]}]
(ptk/reify ::toggle-token-set
ptk/WatchEvent
(watch [it state _]
(let [theme (some-> (wtts/update-theme-id state)
(wtts/get-workspace-token-theme state))
(let [target-theme-id (wtts/get-temp-theme-id state)
active-set-ids (wtts/get-active-set-ids state)
theme (-> (wtts/get-workspace-token-theme target-theme-id state)
(assoc :sets active-set-ids))
changes (-> (pcb/empty-changes it)
(pcb/update-token-theme
(wtts/toggle-token-set-to-token-theme token-set-id theme)
theme)
(pcb/update-active-token-themes #{(wtts/update-theme-id state)} (wtts/get-active-theme-ids state)))]
(pcb/update-active-token-themes #{target-theme-id} (wtts/get-active-theme-ids state)))]
(rx/of
(dch/commit-changes changes)
(wtu/update-workspace-tokens))))))

View file

@ -240,9 +240,16 @@
st/state
=))
(defn workspace-token-theme
[id]
(l/derived #(wtts/get-workspace-theme id %) st/state))
(def workspace-active-theme-ids
(l/derived wtts/get-active-theme-ids st/state))
(def workspace-temp-theme-id
(l/derived wtts/get-temp-theme-id st/state))
(def workspace-active-set-ids
(l/derived wtts/get-active-set-ids st/state))
@ -252,16 +259,19 @@
(def workspace-ordered-token-themes
(l/derived wtts/get-workspace-ordered-themes st/state))
(def workspace-token-sets
(def workspace-ordered-token-sets
(l/derived
(fn [data]
(or (wtts/get-workspace-sets data) {}))
(or (wtts/get-workspace-ordered-sets data) {}))
st/state
=))
(def workspace-active-theme-sets-tokens
(l/derived wtts/get-active-theme-sets-tokens-names-map st/state =))
(def workspace-ordered-token-sets-tokens
(l/derived wtts/get-workspace-ordered-sets-tokens st/state =))
(def workspace-selected-token-set-tokens
(l/derived
(fn [data]

View file

@ -6,6 +6,7 @@
(ns app.main.ui
(:require
[app.main.ui.workspace.tokens.modals.themes :as wtmt]
[app.config :as cf]
[app.main.refs :as refs]
[app.main.store :as st]

View file

@ -144,7 +144,7 @@ Token names should only contain letters and digits separated by . characters.")}
(mf/defc form
{::mf/wrap-props false}
[{:keys [token token-type] :as _args}]
(let [selected-set-tokens (mf/deref refs/workspace-selected-token-set-tokens)
(let [tokens (mf/deref refs/workspace-ordered-token-sets-tokens)
active-theme-tokens (mf/deref refs/workspace-active-theme-sets-tokens)
resolved-tokens (sd/use-resolved-tokens active-theme-tokens {:names-map? true
:cache-atom form-token-cache-atom})
@ -152,9 +152,9 @@ Token names should only contain letters and digits separated by . characters.")}
(mf/deps (:name token))
#(wtt/token-name->path (:name token)))
selected-set-tokens-tree (mf/use-memo
(mf/deps token-path selected-set-tokens)
(mf/deps token-path tokens)
(fn []
(-> (wtt/token-names-tree selected-set-tokens)
(-> (wtt/token-names-tree tokens)
;; Allow setting editing token to it's own path
(d/dissoc-in token-path))))

View file

@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as modal]
[app.main.ui.workspace.tokens.modals.themes :as wtmt]
[app.main.refs :as refs]
[app.main.ui.workspace.tokens.form :refer [form]]
[okulary.core :as l]
@ -37,7 +38,7 @@
(-> (calculate-position vport position x y)
(clj->js))))
(mf/defc modal
(mf/defc token-update-create-modal
{::mf/wrap-props false}
[{:keys [x y position token token-type] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position)]
@ -47,82 +48,88 @@
[:& form {:token token
:token-type token-type}]]))
(mf/defc token-themes-modal
{::mf/register modal/components
::mf/register-as :tokens/themes}
[args]
[:& wtmt/modal args])
;; Modals ----------------------------------------------------------------------
(mf/defc boolean-modal
{::mf/register modal/components
::mf/register-as :tokens/boolean}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc border-radius-modal
{::mf/register modal/components
::mf/register-as :tokens/border-radius}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc stroke-width-modal
{::mf/register modal/components
::mf/register-as :tokens/stroke-width}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc box-shadow-modal
{::mf/register modal/components
::mf/register-as :tokens/box-shadow}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc sizing-modal
{::mf/register modal/components
::mf/register-as :tokens/sizing}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc dimensions-modal
{::mf/register modal/components
::mf/register-as :tokens/dimensions}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc numeric-modal
{::mf/register modal/components
::mf/register-as :tokens/numeric}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc opacity-modal
{::mf/register modal/components
::mf/register-as :tokens/opacity}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc other-modal
{::mf/register modal/components
::mf/register-as :tokens/other}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc rotation-modal
{::mf/register modal/components
::mf/register-as :tokens/rotation}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc spacing-modal
{::mf/register modal/components
::mf/register-as :tokens/spacing}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc string-modal
{::mf/register modal/components
::mf/register-as :tokens/string}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])
(mf/defc typography-modal
{::mf/register modal/components
::mf/register-as :tokens/typography}
[properties]
[:& modal properties])
[:& token-update-create-modal properties])

View file

@ -0,0 +1,237 @@
;; 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.workspace.tokens.modals.themes
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.modal :as modal]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.tokens.common :refer [labeled-input]]
[app.main.ui.workspace.tokens.sets :as wts]
[app.main.ui.workspace.tokens.token-set :as wtts]
[app.util.dom :as dom]
[rumext.v2 :as mf]
[cuerdas.core :as str]
[app.main.ui.workspace.tokens.sets-context :as sets-context]))
(def ^:private chevron-icon
(i/icon-xref :arrow (stl/css :chevron-icon)))
(def ^:private close-icon
(i/icon-xref :close (stl/css :close-icon)))
(mf/defc empty-themes
[{:keys [set-state]}]
[:div {:class (stl/css :empty-themes-wrapper)}
[:div {:class (stl/css :empty-themes-message)}
[:h1 "You currently have no themes."]
[:p "Create your first theme now."]]
[:div {:class (stl/css :button-footer)}
[:button {:class (stl/css :button-primary)
:on-click #(set-state (fn [_] {:type :create-theme}))}
"New theme"]]])
(mf/defc switch
[{:keys [selected? name on-change]}]
(let [selected (if selected? :on :off)]
[:& radio-buttons {:selected selected
:on-change on-change
:name name}
[:& radio-button {:id :on
:value :on
:icon i/tick
:label ""}]
[:& radio-button {:id :off
:value :off
:icon i/close
:label ""}]]))
(mf/defc themes-overview
[{:keys [set-state]}]
(let [active-theme-ids (mf/deref refs/workspace-active-theme-ids)
themes (mf/deref refs/workspace-ordered-token-themes)
on-edit-theme (fn [theme e]
(dom/prevent-default e)
(dom/stop-propagation e)
(set-state (fn [_] {:type :edit-theme
:theme-id (:id theme)})))]
[:div
[:ul {:class (stl/css :theme-group-wrapper)}
(for [[group themes] themes]
[:li {:key (str "token-theme-group" group)}
(when (seq group)
[:span {:class (stl/css :theme-group-label)} group])
[:ul {:class (stl/css :theme-group-rows-wrapper)}
(for [{:keys [id name] :as theme} themes
:let [selected? (some? (get active-theme-ids id))]]
[:li {:key (str "token-theme-" id)
:class (stl/css :theme-row)}
[:div {:class (stl/css :theme-row-left)}
[:div {:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme id)))}
[:& switch {:name (str "Theme" name)
:on-change (constantly nil)
:selected? selected?}]]
[:span {:class (stl/css :theme-row-label)} name]]
[:div {:class (stl/css :theme-row-right)}
(if-let [sets-count (some-> theme :sets seq count)]
[:button {:class (stl/css :sets-count-button)
:on-click #(on-edit-theme theme %)}
(str sets-count " sets")
chevron-icon]
[:button {:class (stl/css :sets-count-empty-button)
:on-click #(on-edit-theme theme %)}
"No sets defined"
chevron-icon])
[:div {:class (stl/css :delete-theme-button)}
[:button {:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/delete-token-theme id)))}
i/delete]]]])]])]
[:div {:class (stl/css :button-footer)}
[:button {:class (stl/css :create-theme-button)
:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(set-state (fn [_] {:type :create-theme})))}
i/add
"Create theme"]]]))
(mf/defc edit-theme
[{:keys [token-sets theme on-back on-submit] :as props}]
(let [edit? (some? (:id theme))
theme-state (mf/use-state {:token-sets token-sets
:theme theme})
disabled? (-> (get-in @theme-state [:theme :name])
(str/trim)
(str/empty?))
token-set-active? (mf/use-callback
(mf/deps theme-state)
(fn [id]
(get-in @theme-state [:theme :sets id])))
on-toggle-token-set (mf/use-callback
(mf/deps theme-state)
(fn [token-set-id]
(swap! theme-state (fn [st]
(update st :theme #(wtts/toggle-token-set-to-token-theme token-set-id %))))))
on-change-field (fn [field]
(fn [e]
(swap! theme-state (fn [st] (assoc-in st field (dom/get-target-val e))))))
on-update-group (on-change-field [:theme :group])
on-update-name (on-change-field [:theme :name])
on-save-form (mf/use-callback
(mf/deps theme-state on-submit)
(fn [e]
(dom/prevent-default e)
(let [theme (:theme @theme-state)
final-name (str/trim (:name theme))
final-group (-> (:group theme)
(str/trim)
(str/lower))]
(when-not (str/empty? final-name)
(cond-> theme
(empty final-group) (dissoc :group)
:always on-submit)))
(on-back)))]
[:form {:on-submit on-save-form}
[:div {:class (stl/css :edit-theme-wrapper)}
[:div
[:button {:class (stl/css :back-button)
:type "button"
:on-click on-back}
chevron-icon "Back"]]
[:div {:class (stl/css :edit-theme-inputs-wrapper)}
[:& labeled-input {:label "Group"
:input-props {:default-value (:group theme)
:on-change on-update-group}}]
[:& labeled-input {:label "Theme"
:input-props {:default-value (:name theme)
:on-change on-update-name}}]]
[:div {:class (stl/css :sets-list-wrapper)}
[:& wts/controlled-sets-list
{:token-sets token-sets
:token-set-selected? (constantly false)
:token-set-active? token-set-active?
:on-select on-toggle-token-set
:on-toggle-token-set on-toggle-token-set
:context sets-context/static-context}]]
[:div {:class (stl/css :edit-theme-footer)}
(if edit?
[:button {:class (stl/css :button-secondary)
:type "button"
:on-click (fn []
(st/emit! (wdt/delete-token-theme (:id theme)))
(on-back))}
"Delete"]
[:div])
[:div {:class (stl/css :button-footer)}
[:button {:class (stl/css :button-secondary)
:type "button"
:on-click #(st/emit! (modal/hide))}
"Cancel"]
[:button {:class (stl/css :button-primary)
:type "submit"
:on-click on-save-form
:disabled disabled?}
"Save theme"]]]]]))
(mf/defc controlled-edit-theme
[{:keys [state set-state]}]
(let [{:keys [theme-id]} @state
token-sets (mf/deref refs/workspace-ordered-token-sets)
theme (mf/deref (refs/workspace-token-theme theme-id))]
[:& edit-theme
{:token-sets token-sets
:theme theme
:on-back #(set-state (constantly {:type :themes-overview}))
:on-submit #(st/emit! (wdt/update-token-theme %))}]))
(mf/defc create-theme
[{:keys [set-state]}]
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
theme {:name "" :sets #{}}]
[:& edit-theme
{:token-sets token-sets
:theme theme
:on-back #(set-state (constantly {:type :themes-overview}))
:on-submit #(st/emit! (wdt/create-token-theme %))}]))
(mf/defc themes
[{:keys [] :as _args}]
(let [themes (mf/deref refs/workspace-ordered-token-themes)
state (mf/use-state (if (empty? themes)
{:type :create-theme}
{:type :themes-overview}))
set-state (mf/use-callback #(swap! state %))
title (case (:type @state)
:edit-theme "Edit Theme"
"Themes")
component (case (:type @state)
:empty-themes empty-themes
:themes-overview (if (empty? themes) empty-themes themes-overview)
:edit-theme controlled-edit-theme
:create-theme create-theme)]
[:div
[:div {:class (stl/css :modal-title)} title]
[:div {:class (stl/css :modal-content)}
[:& component {:state state
:set-state set-state}]]]))
(mf/defc modal
{::mf/wrap-props false}
[{:keys [] :as _args}]
(let [handle-close-dialog (mf/use-callback #(st/emit! (modal/hide)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:& themes]]]))

View file

@ -0,0 +1,213 @@
// 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";
.modal-overlay {
@extend .modal-overlay-base;
}
hr {
border-color: var(--color-background-tertiary);
}
.modal-dialog {
@extend .modal-container-base;
display: grid;
grid-template-rows: auto 1fr auto;
width: 100%;
max-width: $s-468;
user-select: none;
}
.modal-title {
@include headlineMediumTypography;
font-weight: 500;
margin-block-end: $s-16;
color: var(--color-foreground-secondary);
}
.modal-content {
display: flex;
flex-direction: column;
}
.button-footer {
display: flex;
justify-content: flex-end;
gap: $s-6;
}
.edit-theme-footer {
display: flex;
justify-content: space-between;
}
.button-primary {
@extend .button-primary;
padding: $s-6;
}
.button-secondary {
@extend .button-secondary;
padding: $s-6;
}
.empty-themes-wrapper {
display: flex;
flex-direction: column;
.empty-themes-message {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: $s-12;
padding: $s-72 0;
h1 {
@include headlineLargeTypography;
}
p {
@include bodyMediumTypography;
font-weight: 500;
color: var(--color-foreground-secondary);
}
}
}
.create-theme-button {
@extend .button-secondary;
padding: $s-6;
padding-right: $s-8;
svg {
margin-right: $s-6;
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.close-btn {
@extend .modal-close-btn-base;
}
.theme-group-label {
display: block;
@include headlineMediumTypography;
color: var(--color-foreground-secondary);
margin-bottom: $s-8;
}
.theme-group-rows-wrapper {
display: flex;
flex-direction: column;
gap: $s-6;
}
.theme-group-wrapper {
display: flex;
flex-direction: column;
gap: $s-8;
}
.theme-row {
display: flex;
align-items: center;
gap: $s-12;
justify-content: space-between;
}
.theme-row-left {
display: flex;
align-items: center;
gap: $s-16;
}
.theme-row-right {
display: flex;
align-items: center;
gap: $s-6;
}
.back-button {
@extend .button-tertiary;
padding: $s-6;
padding-left: 0;
display: flex;
svg {
scale: -1 1;
margin-left: 0;
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.sets-count-button {
@extend .button-secondary;
padding: $s-6;
padding-left: $s-12;
svg {
margin-left: $s-6;
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.edit-theme-wrapper {
display: flex;
flex-direction: column;
gap: $s-12;
}
.edit-theme-inputs-wrapper {
display: grid;
grid-template-columns: 0.6fr 1fr;
gap: $s-12;
}
.sets-list-wrapper {
border: 1px solid color-mix(in hsl, var(--color-foreground-secondary) 30%, transparent);
border-radius: $s-8;
overflow: hidden;
}
.sets-count-empty-button {
@extend .button-secondary;
padding: $s-6;
padding-left: $s-12;
svg {
margin-left: $s-6;
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.theme-row-label {
@include bodyMediumTypography;
font-weight: 500;
color: var(--color-foreground-primary);
}
.delete-theme-button {
@extend .button-tertiary;
height: $s-28;
width: $s-28;
button {
@include buttonStyle;
@include flexCenter;
width: $s-24;
height: 100%;
svg {
@extend .button-icon-small;
height: $s-12;
width: $s-12;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
}
}

View file

@ -12,32 +12,81 @@
[app.main.store :as st]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
[rumext.v2 :as mf]
[app.main.ui.workspace.tokens.sets-context :as sets-context]))
(def ^:private chevron-icon
(i/icon-xref :arrow (stl/css :chevron-icon)))
(defn on-toggle-token-set-click [id event]
(dom/stop-propagation event)
(st/emit! (wdt/toggle-token-set id)))
(defn on-toggle-token-set-click [token-set-id]
(st/emit! (wdt/toggle-token-set {:token-set-id token-set-id})))
(defn on-select-token-set-click [id event]
(defn on-select-token-set-click [id]
(st/emit! (wdt/set-selected-token-set-id id)))
(defn on-delete-token-set-click [id event]
(dom/stop-propagation event)
(st/emit! (wdt/delete-token-set id)))
(defn on-update-token-set [token-set]
(st/emit! (wdt/update-token-set token-set)))
(defn on-create-token-set [token-set]
(st/emit! (wdt/create-token-set token-set)))
(mf/defc editing-node
[{:keys [default-value on-cancel on-submit]}]
(let [ref (mf/use-ref)
on-submit-valid (mf/use-fn
(fn [event]
(let [value (str/trim (dom/get-target-val event))]
(if (or (str/empty? value)
(= value default-value))
(on-cancel)
(on-submit value)))))
on-key-down (mf/use-fn
(fn [event]
(cond
(kbd/enter? event) (on-submit-valid event)
(kbd/esc? event) (on-cancel))))]
[:input
{:class (stl/css :editing-node)
:type "text"
:ref ref
:on-blur on-submit-valid
:on-key-down on-key-down
:auto-focus true
:default-value default-value}]))
(mf/defc sets-tree
[{:keys [token-set token-set-active? token-set-selected?] :as _props}]
[{:keys [token-set
token-set-active?
token-set-selected?
editing?
on-select
on-toggle
on-edit
on-submit
on-cancel]
:as _props}]
(let [{:keys [id name _children]} token-set
selected? (and set? (token-set-selected? id))
visible? (token-set-active? id)
collapsed? (mf/use-state false)
set? true #_(= type :set)
group? false #_(= type :group)]
group? false #_(= type :group)
editing-node? (editing? id)
on-select (mf/use-callback
(mf/deps editing-node?)
(fn [event]
(dom/stop-propagation event)
(when-not editing-node?
(on-select id))))]
[:div {:class (stl/css :set-item-container)
:on-click #(on-select-token-set-click id %)}
:on-click on-select
:on-double-click #(on-edit id)}
[:div {:class (stl/css-case :set-item-group group?
:set-item-set set?
:selected-set selected?)}
@ -48,25 +97,72 @@
chevron-icon])
[:span {:class (stl/css :icon)}
(if set? i/document i/group)]
[:div {:class (stl/css :set-name)} name]
[:div {:class (stl/css :delete-set)}
[:button {:on-click #(on-delete-token-set-click id %)}
i/delete]]
(when set?
[:span {:class (stl/css :action-btn)
:on-click #(on-toggle-token-set-click id %)}
(if visible? i/shown i/hide)])]
#_(when (and children (not @collapsed?))
[:div {:class (stl/css :set-children)}
(for [child-id children]
[:& sets-tree (assoc props :key child-id
{:key child-id}
:set-id child-id
:selected-set-id selected-token-set-id)])])]))
(if editing-node?
[:& editing-node {:default-value name
:on-submit #(on-submit (assoc token-set :name %))
:on-cancel on-cancel}]
[:*
[:div {:class (stl/css :set-name)} name]
[:div {:class (stl/css :delete-set)}
[:button {:on-click #(on-delete-token-set-click id %)
:type "button"}
i/delete]]
(if set?
[:span {:class (stl/css :action-btn)
:on-click (fn [event]
(dom/stop-propagation event)
(on-toggle id))}
(if visible? i/shown i/hide)]
nil
#_(when (and children (not @collapsed?))
[:div {:class (stl/css :set-children)}
(for [child-id children]
[:& sets-tree (assoc props :key child-id
{:key child-id}
:set-id child-id
:selected-set-id selected-token-set-id)])]))])]]))
(mf/defc controlled-sets-list
[{:keys [token-sets
on-update-token-set
token-set-selected?
token-set-active?
on-create-token-set
on-toggle-token-set
on-select
context]
:as _props}]
(let [{:keys [editing? new? on-edit on-create on-reset] :as ctx} (or context (sets-context/use-context))]
[:ul {:class (stl/css :sets-list)}
(for [[id token-set] token-sets]
(when token-set
[:& sets-tree {:key id
:token-set token-set
:token-set-selected? (if new? (constantly false) token-set-selected?)
:token-set-active? token-set-active?
:editing? editing?
:on-select on-select
:on-edit on-edit
:on-toggle on-toggle-token-set
:on-submit #(do
(on-update-token-set %)
(on-reset))
:on-cancel on-reset}]))
(when new?
[:& sets-tree {:token-set {:name ""}
:token-set-selected? (constantly true)
:token-set-active? (constantly true)
:editing? (constantly true)
:on-select (constantly nil)
:on-edit on-create
:on-submit #(do
(on-create-token-set %)
(on-reset))
:on-cancel on-reset}])]))
(mf/defc sets-list
[{:keys []}]
(let [token-sets (mf/deref refs/workspace-token-sets)
(let [token-sets (mf/deref refs/workspace-ordered-token-sets)
selected-token-set-id (mf/deref refs/workspace-selected-token-set-id)
token-set-selected? (mf/use-callback
(mf/deps selected-token-set-id)
@ -77,11 +173,11 @@
(mf/deps active-token-set-ids)
(fn [id]
(get active-token-set-ids id)))]
[:ul {:class (stl/css :sets-list)}
(for [[id token-set] token-sets]
[:& sets-tree
{:key id
:token-set token-set
:selected-token-set-id selected-token-set-id
:token-set-selected? token-set-selected?
:token-set-active? token-set-active?}])]))
[:& controlled-sets-list
{:token-sets token-sets
:token-set-selected? token-set-selected?
:token-set-active? token-set-active?
:on-select on-select-token-set-click
:on-toggle-token-set on-toggle-token-set-click
:on-update-token-set on-update-token-set
:on-create-token-set on-create-token-set}]))

View file

@ -126,3 +126,21 @@
transform: rotate(var(--chevron-icon-rotation));
stroke: var(--icon-foreground);
}
.editing-node {
@include textEllipsis;
color: var(--layer-row-foreground-color-focus);
}
input.editing-node {
@include textEllipsis;
@include bodySmallTypography;
@include removeInputStyle;
flex-grow: 1;
height: $s-28;
padding-left: $s-6;
margin: 0;
border-radius: $br-8;
border: $s-1 solid var(--input-border-color-focus);
color: var(--layer-row-foreground-color);
}

View file

@ -0,0 +1,41 @@
(ns app.main.ui.workspace.tokens.sets-context
(:require
[rumext.v2 :as mf]))
(def initial {:editing-id nil
:new? false})
(def context (mf/create-context initial))
(def static-context
{:editing? (constantly false)
:new? false
:on-edit (constantly nil)
:on-create (constantly nil)
:on-reset (constantly nil)})
(mf/defc provider
{::mf/wrap-props false}
[props]
(let [children (unchecked-get props "children")
state (mf/use-state initial)]
[:& (mf/provider context) {:value state}
children]))
(defn use-context []
(let [ctx (mf/use-ctx context)
{:keys [editing-id new?]} @ctx
editing? (mf/use-callback
(mf/deps editing-id)
#(= editing-id %))
on-edit (mf/use-fn
#(swap! ctx assoc :editing-id %))
on-create (mf/use-fn
#(swap! ctx assoc :editing-id (random-uuid) :new? true))
on-reset (mf/use-fn
#(reset! ctx initial))]
{:editing? editing?
:new? new?
:on-edit on-edit
:on-create on-create
:on-reset on-reset}))

View file

@ -14,6 +14,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar]]
[app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as i]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.tokens.changes :as wtch]
@ -21,7 +22,9 @@
[app.main.ui.workspace.tokens.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.core :as wtc]
[app.main.ui.workspace.tokens.sets :refer [sets-list]]
[app.main.ui.workspace.tokens.sets-context :as sets-context]
[app.main.ui.workspace.tokens.style-dictionary :as sd]
[app.main.ui.workspace.tokens.theme-select :refer [theme-select]]
[app.main.ui.workspace.tokens.token :as wtt]
[app.main.ui.workspace.tokens.token-types :as wtty]
[app.util.dom :as dom]
@ -107,6 +110,7 @@
(let [{:keys [key fields]} modal]
(dom/stop-propagation event)
(st/emit! (dt/set-token-type-section-open type true))
(js/console.log "key" key)
(modal/show! key {:x (.-clientX ^js event)
:y (.-clientY ^js event)
:position :right
@ -179,74 +183,47 @@
:name @name}))}
"Create"]]))
(mf/defc edit-button
[{:keys [create?]}]
[:button {:class (stl/css :themes-button)
:on-click (fn [e]
(dom/stop-propagation e)
(modal/show! :tokens/themes {}))}
(if create? "Create" "Edit")])
(mf/defc themes-sidebar
[_props]
(let [open? (mf/use-state true)
active-theme-ids (mf/deref refs/workspace-active-theme-ids)
themes (mf/deref refs/workspace-ordered-token-themes)]
[:div {:class (stl/css :sets-sidebar)}
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title "THEMES"
:on-collapsed #(swap! open? not)}]]
(when @open?
[:div
[:style
(str "@scope {"
(str/join "\n"
["ul { list-style-type: circle; margin-left: 20px; }"
".spaced { display: flex; gap: 10px; justify-content: space-between; }"
".spaced-y { display: flex; flex-direction: column; gap: 10px }"
".selected { font-weight: 600; }"
"b { font-weight: 600; }"])
"}")]
[:div.spaced-y
{:style {:padding "10px"}}
[:& tokene-theme-create]
[:div.spaced-y
[:b "Themes"]
[:ul
(for [[group themes] themes]
[:li
{:key (str "token-theme-group" group)}
group
[:ul
(for [{:keys [id name] :as _theme} themes]
[:li {:key (str "tokene-theme-" id)}
[:div.spaced
name
[:div.spaced
[:button
{:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme id)))}
(if (get active-theme-ids id) "✅" "❎")]
[:button {:on-click (fn [e]
(dom/prevent-default e)
(dom/stop-propagation e)
(st/emit! (wdt/delete-token-theme id)))}
"🗑️"]]]])]])]]]])]))
(let [ordered-themes (mf/deref refs/workspace-ordered-token-themes)]
[:div {:class (stl/css :theme-sidebar)}
[:span {:class (stl/css :themes-header)} "Themes"]
[:div {:class (stl/css :theme-select-wrapper)}
[:& theme-select]
[:& edit-button {:create? (empty? ordered-themes)}]]]))
(mf/defc add-set-button
[{:keys [on-open]}]
(let [{:keys [on-create]} (sets-context/use-context)]
[:button {:class (stl/css :add-set)
:on-click #(do
(on-open)
(on-create))}
i/add]))
(mf/defc sets-sidebar
[]
(let [open? (mf/use-state true)]
[:div {:class (stl/css :sets-sidebar)}
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title "SETS"
:on-collapsed #(swap! open? not)}]
[:button {:class (stl/css :add-set)
:on-click #(do
(reset! open? true)
(on-set-add-click %))}
i/add]]
(when @open?
[:& sets-list])]))
(let [open? (mf/use-state true)
on-open (mf/use-fn #(reset! open? true))]
[:& sets-context/provider {}
[:div {:class (stl/css :sets-sidebar)}
[:div {:class (stl/css :sidebar-header)}
[:& title-bar {:collapsable true
:collapsed (not @open?)
:all-clickable true
:title "SETS"
:on-collapsed #(swap! open? not)}
[:& add-set-button {:on-open on-open}]]]
(when @open?
[:& sets-list])]]))
(mf/defc tokens-explorer
[_props]
@ -306,13 +283,24 @@
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[_props]
(let [show-sets-section? (deref (temp-use-themes-flag))]
(let [show-sets-section? (deref (temp-use-themes-flag))
{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 400 :y false nil)]
[:div {:class (stl/css :sidebar-tab-wrapper)}
(when show-sets-section?
[:div {:class (stl/css :sets-section-wrapper)}
[:div {:class (stl/css :sets-section-wrapper)
:style {:height (str size-pages-opened "px")}}
[:& themes-sidebar]
[:& sets-sidebar]])
[:div {:class (stl/css :tokens-section-wrapper)}
(when show-sets-section?
[: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}])
[:& tokens-explorer]]
[:button {:class (stl/css :download-json-button)
:on-click wtc/download-tokens-as-json}

View file

@ -18,12 +18,18 @@
display: flex;
flex-direction: column;
margin-bottom: $s-8;
overflow-y: auto;
}
.sets-sidebar {
position: relative;
}
.theme-sidebar {
padding: $s-12;
padding-bottom: 0;
}
.sidebar-header {
display: flex;
align-items: center;
@ -114,3 +120,30 @@
height: 20px;
}
}
.theme-select-wrapper {
display: grid;
grid-template-columns: 1fr 0.28fr;
gap: $s-6;
}
.themes-button {
@extend .button-secondary;
width: auto;
}
.themes-header {
display: block;
@include headlineSmallTypography;
margin-bottom: $s-8;
padding-left: $s-8;
color: var(--title-foreground-color);
}
.resize-area-horiz {
position: absolute;
left: 0;
width: 100%;
border-bottom: $s-2 solid var(--resize-area-border-color);
cursor: ns-resize;
}

View file

@ -0,0 +1,95 @@
;; 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.workspace.tokens.theme-select
(:require-macros [app.main.style :as stl])
(:require
[app.common.uuid :as uuid]
[app.main.data.modal :as modal]
[app.main.data.tokens :as wdt]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(mf/defc themes-list
[{:keys [themes active-theme-ids on-close grouped?]}]
(when (seq themes)
[:ul
(for [{:keys [id name]} themes
:let [selected? (get active-theme-ids id)]]
[:li {:key id
:class (stl/css-case
:checked-element true
:sub-item grouped?
:is-selected selected?)
:on-click (fn [e]
(dom/stop-propagation e)
(st/emit! (wdt/toggle-token-theme id))
(on-close))}
[:span {:class (stl/css :label)} name]
[:span {:class (stl/css :check-icon)} i/tick]])]))
(mf/defc theme-options
[{:keys [on-close]}]
(let [active-theme-ids (mf/deref refs/workspace-active-theme-ids)
ordered-themes (mf/deref refs/workspace-ordered-token-themes)
grouped-themes (dissoc ordered-themes nil)
ungrouped-themes (get ordered-themes nil)]
[:ul
[:& themes-list {:themes ungrouped-themes
:active-theme-ids active-theme-ids
:on-close on-close}]
(for [[group themes] grouped-themes]
[:li {:key group}
(when group
[:span {:class (stl/css :group)} group])
[:& themes-list {:themes themes
:active-theme-ids active-theme-ids
:on-close on-close
:grouped? true}]])
[:li {:class (stl/css-case :checked-element true
:checked-element-button true)
:on-click #(modal/show! :tokens/themes {})}
[:span "Edit themes"]
[:span {:class (stl/css :icon)} i/arrow]]]))
(mf/defc theme-select
[{:keys []}]
(let [;; Store
temp-theme-id (mf/deref refs/workspace-temp-theme-id)
active-theme-ids (-> (mf/deref refs/workspace-active-theme-ids)
(disj temp-theme-id))
active-themes-count (count active-theme-ids)
themes (mf/deref refs/workspace-token-themes)
;; Data
current-label (cond
(> active-themes-count 1) (str active-themes-count " themes active")
(pos? active-themes-count) (get-in themes [(first active-theme-ids) :name])
:else "No theme active")
;; State
state* (mf/use-state
{:id (uuid/next)
:is-open? false})
state (deref state*)
is-open? (:is-open? state)
;; Dropdown
dropdown-element* (mf/use-ref nil)
on-close-dropdown (mf/use-fn #(swap! state* assoc :is-open? false))
on-open-dropdown (mf/use-fn #(swap! state* assoc :is-open? true))]
[:div {:on-click on-open-dropdown
:class (stl/css :custom-select)}
[:span {:class (stl/css :current-label)} current-label]
[:span {:class (stl/css :dropdown-button)} i/arrow]
[:& dropdown {:show is-open? :on-close on-close-dropdown}
[:div {:ref dropdown-element*
:class (stl/css :custom-select-dropdown)}
[:& theme-options {:on-close on-close-dropdown}]]]]))

View file

@ -0,0 +1,167 @@
// 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";
.custom-select {
--border-color: var(--menu-background-color);
--bg-color: var(--menu-background-color);
--icon-color: var(--icon-foreground);
--text-color: var(--menu-foreground-color);
@extend .new-scrollbar;
@include bodySmallTypography;
position: relative;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
height: $s-32;
width: 100%;
margin: 0;
padding: $s-8;
border-radius: $br-8;
background-color: var(--bg-color);
border: $s-1 solid var(--border-color);
color: var(--text-color);
cursor: pointer;
ul {
margin-bottom: 0;
}
.group {
display: block;
@include headlineSmallTypography;
padding: $s-8;
color: var(--color-foreground-secondary);
font-weight: 600;
}
&.icon {
grid-template-columns: auto 1fr auto;
}
&:hover {
--bg-color: var(--menu-background-color-hover);
--border-color: var(--menu-background-color);
--icon-color: var(--menu-foreground-color-hover);
}
&:focus {
--bg-color: var(--menu-background-color-focus);
--border-color: var(--menu-background-focus);
}
}
.disabled {
--bg-color: var(--menu-background-color-disabled);
--border-color: var(--menu-border-color-disabled);
--icon-color: var(--menu-foreground-color-disabled);
--text-color: var(--menu-foreground-color-disabled);
pointer-events: none;
cursor: default;
}
.dropdown-button {
@include flexCenter;
svg {
@extend .button-icon-small;
transform: rotate(90deg);
stroke: var(--icon-color);
}
}
.current-icon {
@include flexCenter;
width: $s-24;
padding-right: $s-4;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.custom-select-dropdown {
@extend .dropdown-wrapper;
.separator {
margin: 0;
height: $s-12;
border-block-start: $s-1 solid var(--dropdown-separator-color);
}
}
.custom-select-dropdown[data-direction="up"] {
bottom: $s-32;
top: auto;
}
.sub-item {
padding-left: $s-16;
}
.checked-element-button {
@extend .dropdown-element-base;
position: relative;
display: flex;
justify-content: space-between;
padding-right: 0;
}
li + .checked-element-button {
margin-top: $s-8;
&:before {
content: "";
position: absolute;
top: -$s-4;
left: 0;
right: 0;
height: 1px;
background-color: color-mix(in hsl, var(--color-foreground-secondary) 20%, transparent);
}
}
.checked-element {
@extend .dropdown-element-base;
.icon {
@include flexCenter;
height: $s-24;
width: $s-24;
padding-right: $s-4;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.label {
flex-grow: 1;
width: 100%;
}
.check-icon {
@include flexCenter;
svg {
@extend .button-icon-small;
visibility: hidden;
stroke: var(--icon-foreground);
}
}
&.is-selected {
color: var(--menu-foreground-color);
.check-icon svg {
stroke: var(--menu-foreground-color);
visibility: visible;
}
}
&.disabled {
display: none;
}
}
.current-label {
@include textEllipsis;
}

View file

@ -12,6 +12,9 @@
(defn get-workspace-themes [state]
(get-in state [:workspace-data :token-themes] []))
(defn get-workspace-theme [id state]
(get-in state [:workspace-data :token-themes-index id]))
(defn get-workspace-themes-index [state]
(get-in state [:workspace-data :token-themes-index] {}))
@ -69,7 +72,7 @@
(defn toggle-active-theme-id
"Toggle a `theme-id` by checking `:token-active-themes`.
De-activate all theme-ids that have the same group as `theme-id` when activating `theme-id`.
Deactivate all theme-ids that have the same group as `theme-id` when activating `theme-id`.
Ensures that the temporary theme id is selected when the resulting set is empty."
[theme-id state]
(let [temp-theme-id-set (some->> (get-temp-theme-id state) (conj #{}))
@ -112,6 +115,24 @@
(defn get-workspace-sets [state]
(get-in state [:workspace-data :token-sets-index]))
(defn get-workspace-ordered-sets [state]
;; TODO Include groups
(let [top-level-set-ids (get-in state [:workspace-data :token-set-groups])
token-sets (get-workspace-sets state)]
(->> (map (fn [id] [id (get token-sets id)]) top-level-set-ids)
(into (ordered-map)))))
(defn get-workspace-ordered-sets-tokens [state]
(let [sets (get-workspace-ordered-sets state)]
(reduce
(fn [acc [_ {:keys [tokens] :as sets}]]
(reduce (fn [acc' token-id]
(if-let [token (wtt/get-workspace-token token-id state)]
(assoc acc' (wtt/token-identifier token) token)
acc'))
acc tokens))
{} sets)))
(defn get-token-set [set-id state]
(some-> (get-workspace-sets state)
(get set-id)))