Compare commits

..

6 Commits

Author SHA1 Message Date
Pablo Alba
094ad897ab 🐛 Fix migration sync-component-id-with-near-main (loop added) 2026-01-07 16:24:16 +01:00
Pablo Alba
b2c45f6e1d 🐛 Fix migration and repair for nil ids 2026-01-07 16:24:16 +01:00
Pablo Alba
7a377258d3 🐛 Fix detach fails on nested components 2026-01-07 16:24:16 +01:00
Andrés Moya
dd4a18a3ff 🔧 Validate only after propagation in tests 2026-01-07 16:24:16 +01:00
Andrés Moya
bff005a951 🔧 Avoid copies with wrong component-id and repair if needed 2026-01-07 16:24:16 +01:00
Andrés Moya
776c62051d 🔧 Validate components with unneeded objects 2026-01-07 15:27:11 +01:00
228 changed files with 28016 additions and 34706 deletions

View File

@@ -10,11 +10,9 @@
### :sparkles: New features & Enhancements
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
### :bug: Bugs fixed
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
@@ -38,13 +36,7 @@
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
- Fix problem with grid layout components and auto sizing [Github #7797](https://github.com/penpot/penpot/issues/7797)
- Fix some alignments on inspect tab [Taiga #12915](https://tree.taiga.io/project/penpot/issue/12915)
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
- Fix color assets from shared libraries not appearing as assets in Selected colors panel [Taiga #12957](https://tree.taiga.io/project/penpot/issue/12957)
- Fix CSS generated box-shadow property [Taiga #12997](https://tree.taiga.io/project/penpot/issue/12997)
- Fix inner shadow selector on shadow token [Taiga #12951](https://tree.taiga.io/project/penpot/issue/12951)
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
## 2.12.1
@@ -54,6 +46,7 @@
- Fix problem with style in fonts input [Taiga #12935](https://tree.taiga.io/project/penpot/issue/12935)
- Fix problem with path editor and right click [Github #7917](https://github.com/penpot/penpot/issues/7917)
## 2.12.0
### :boom: Breaking changes & Deprecations
@@ -65,6 +58,7 @@ The backend RPC API URLS are changed from `/api/rpc/command/<name>` to
compatibility; however, if you are a user of this API, it is strongly
recommended that you adapt your code to use the new PATH.
#### Updated SSO Callback URL
The OAuth / Single Sign-On (SSO) callback endpoint has changed to
@@ -97,6 +91,7 @@ This update standardizes all authentication flows under the single URL
and makis it more modular, enabling the ability to configure SSO auth
provider dinamically.
#### Changes on default docker compose
We have updated the `docker/images/docker-compose.yaml` with a small

View File

@@ -97,8 +97,8 @@
:jmx-remote
{:jvm-opts ["-Dcom.sun.management.jmxremote"
"-Dcom.sun.management.jmxremote.port=9000"
"-Dcom.sun.management.jmxremote.rmi.port=9000"
"-Dcom.sun.management.jmxremote.port=9090"
"-Dcom.sun.management.jmxremote.rmi.port=9090"
"-Dcom.sun.management.jmxremote.local.only=false"
"-Dcom.sun.management.jmxremote.authenticate=false"
"-Dcom.sun.management.jmxremote.ssl=false"

View File

@@ -19,7 +19,7 @@
inner join team_profile_rel as tpr on (tpr.team_id = p.team_id)
where tpr.profile_id = ?
and p.team_id = ?
and (p.deleted_at is null)
and (p.deleted_at is null or p.deleted_at > now())
and (tpr.is_admin = true or
tpr.is_owner = true or
tpr.can_edit = true)
@@ -29,7 +29,7 @@
inner join project_profile_rel as ppr on (ppr.project_id = p.id)
where ppr.profile_id = ?
and p.team_id = ?
and (p.deleted_at is null)
and (p.deleted_at is null or p.deleted_at > now())
and (ppr.is_admin = true or
ppr.is_owner = true or
ppr.can_edit = true)
@@ -47,7 +47,7 @@
left join file_thumbnail as ft on (ft.file_id = f.id and ft.revn = f.revn)
inner join projects as pr on (f.project_id = pr.id)
where f.name ilike ('%' || ? || '%')
and (f.deleted_at is null)
and (f.deleted_at is null or f.deleted_at > now())
order by f.created_at asc")
(defn search-files

View File

@@ -27,7 +27,6 @@
[app.common.types.path :as path]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.common.types.typographies-list :as ctyl]
[app.common.types.typography :as ctt]
@@ -379,7 +378,7 @@
[:type [:= :set-token]]
[:set-id ::sm/uuid]
[:token-id ::sm/uuid]
[:attrs [:maybe cto/schema:token-attrs]]]]
[:attrs [:maybe ctob/schema:token-attrs]]]]
[:set-token-set
[:map {:title "SetTokenSetChange"}

View File

@@ -1766,6 +1766,59 @@
(update :pages-index d/update-vals update-container)
(d/update-when :components d/update-vals update-container))))
(defmethod migrate-data "0017-remove-unneeded-objects-from-components"
[data _]
;; Some components have an `:objects` attribute, despite not being
;; deleted. This migration removes it.
(letfn [(check-component [component]
(if (and (not (:deleted component))
(contains? component :objects))
(dissoc component :objects)
component))]
(d/update-when data :components d/update-vals check-component)))
(defmethod migrate-data "0018-sync-component-id-with-near-main"
[data _]
(let [libs (some-> (:libs data) deref)]
(letfn [(fix-shape
[data page shape]
(if (and (ctk/subcopy-head? shape)
(nil? (ctk/get-swap-slot shape)))
(let [file {:id (:id data) :data data}
ref-shape (ctf/find-ref-shape file page libs shape {:include-deleted? true :with-context? true})]
(if (and (some? ref-shape)
(or (not= (:component-id shape) (:component-id ref-shape))
(not= (:component-file shape) (:component-file ref-shape))))
(cond-> shape
(some? (:component-id ref-shape))
(assoc :component-id (:component-id ref-shape))
(nil? (:component-id ref-shape))
(dissoc :component-id)
(some? (:component-file ref-shape))
(assoc :component-file (:component-file ref-shape))
(nil? (:component-file ref-shape))
(dissoc :component-file))
shape))
shape))
(update-page
[data page]
(d/update-when page :objects d/update-vals (partial fix-shape data page)))
(fix-data [data]
(loop [current-data data
iteration 0]
(let [next-data (update current-data :pages-index d/update-vals (partial update-page current-data))]
(if (or (= current-data next-data)
(> iteration 20)) ;; safety bound
next-data
(recur next-data (inc iteration))))))]
(fix-data data))))
(def available-migrations
(into (d/ordered-set)
["legacy-2"
@@ -1839,4 +1892,6 @@
"0014-clear-components-nil-objects"
"0015-fix-text-attrs-blank-strings"
"0015-clean-shadow-color"
"0016-copy-fills-from-position-data-to-text-node"]))
"0016-copy-fills-from-position-data-to-text-node"
"0017-remove-unneeded-objects-from-components"
"0018-sync-component-id-with-near-main"]))

View File

@@ -333,6 +333,31 @@
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :component-id-mismatch
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
(fn [shape]
; Set the component-id and component-file to the ones of the near main
(log/debug :hint (str " -> set component-id to " (:component-id args)))
(log/debug :hint (str " -> set component-file to " (:component-file args)))
(cond-> shape
(some? (:component-id args))
(assoc :component-id (:component-id args))
(nil? (:component-id args))
(dissoc :component-id)
(some? (:component-file args))
(assoc :component-file (:component-file args))
(nil? (:component-file args))
(dissoc :component-file)))]
(log/dbg :hint "repairing shape :component-id-mismatch" :id (:id shape) :name (:name shape) :page-id page-id)
(-> (pcb/empty-changes nil page-id)
(pcb/with-file-data file-data)
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :ref-shape-is-head
[_ {:keys [shape page-id args] :as error} file-data _]
(let [repair-shape
@@ -499,7 +524,7 @@
(pcb/update-shapes [(:id shape)] repair-shape))))
(defmethod repair-error :component-nil-objects-not-allowed
[_ {:keys [shape] :as error} file-data _]
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
(let [repair-component
(fn [component]
; Remove the objects key, or set it to {} if the component is deleted
@@ -511,10 +536,26 @@
(log/debug :hint " -> remove :objects")
(dissoc component :objects))))]
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id shape) :name (:name shape))
(log/dbg :hint "repairing component :component-nil-objects-not-allowed" :id (:id component) :name (:name component))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id shape) repair-component))))
(pcb/update-component (:id component) repair-component))))
(defmethod repair-error :non-deleted-component-cannot-have-objects
[_ {component :shape} file-data _] ; in this error the :shape argument is the component
(let [repair-component
(fn [component]
; Remove the :objects field
(if-not (:deleted component)
(do
(log/debug :hint " -> remove :objects")
(dissoc component :objects))
component))]
(log/dbg :hint "repairing component :non-deleted-component-cannot-have-objects" :id (:id component) :name (:name component))
(-> (pcb/empty-changes nil)
(pcb/with-library-data file-data)
(pcb/update-component (:id component) repair-component))))
(defmethod repair-error :invalid-text-touched
[_ {:keys [shape page-id] :as error} file-data _]

View File

@@ -8,112 +8,9 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.i18n :refer [tr]]
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[clojure.set :as set]
[cuerdas.core :as str]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HIGH LEVEL SCHEMAS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Token
(defn make-token-name-schema
"Dynamically generates a schema to check a token name, adding translated error messages
and two additional validations:
- Min and max length.
- Checks if other token with a path derived from the name already exists at `tokens-tree`.
e.g. it's not allowed to create a token `foo.bar` if a token `foo` already exists."
[tokens-tree]
[:and
(-> cto/schema:token-name
(sm/update-properties assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error"))))
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
#(not (ctob/token-name-path-exists? % tokens-tree))]])
(def schema:token-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-schema
[tokens-tree]
(sm/merge
cto/schema:token-attrs
[:map
[:name (make-token-name-schema tokens-tree)]
[:description {:optional true} schema:token-description]]))
;; Token set
(defn make-token-set-name-schema
"Generates a dynamic schema to check a token set name:
- Validate name length.
- Checks if other token set with a path derived from the name already exists in the tokens lib."
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
(fn [name]
(let [set (ctob/get-set-by-name tokens-lib name)]
(or (nil? set) (= (ctob/get-id set) set-id))))]])
(def schema:token-set-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-set-schema
[tokens-lib set-id]
(sm/merge
ctob/schema:token-set-attrs
[:map
[:name [:and (make-token-set-name-schema tokens-lib set-id)
[:fn #(ctob/normalized-set-name? %)]]]
[:description {:optional true} schema:token-set-description]]))
;; Token theme
(defn make-token-theme-group-schema
"Generates a dynamic schema to check a token theme group:
- Validate group length.
- Checks if other token theme with the same name already exists in the new group in the tokens lib."
[tokens-lib name theme-id]
[:and
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
(fn [group]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(defn make-token-theme-name-schema
"Generates a dynamic schema to check a token theme name:
- Validate name length.
- Checks if other token theme with the same name already exists in the same group in the tokens lib."
[tokens-lib group theme-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (str group "/" (:value %)))}
(fn [name]
(let [theme (ctob/get-theme-by-name tokens-lib group name)]
(or (nil? theme) (= (:id theme) theme-id))))]])
(def schema:token-theme-description
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}])
(defn make-token-theme-schema
[tokens-lib group name theme-id]
(sm/merge
ctob/schema:token-theme-attrs
[:map
[:group (make-token-theme-group-schema tokens-lib name theme-id)] ;; TODO how to keep error-fn from here?
[:name (make-token-theme-name-schema tokens-lib group theme-id)]
[:description {:optional true} schema:token-theme-description]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def parseable-token-value-regexp
"Regexp that can be used to parse a number value out of resolved token value.
This regexp also trims whitespace around the value."
@@ -183,6 +80,56 @@
(defn shapes-applied-all? [ids-by-attributes shape-ids attributes]
(every? #(set/superset? (get ids-by-attributes %) shape-ids) attributes))
(defn token-name->path
"Splits token-name into a path vector split by `.` characters.
Will concatenate multiple `.` characters into one."
[token-name]
(str/split token-name #"\.+"))
(defn token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (token-name->path token-name)
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `token-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name token-names-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
token-names-tree path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
(defn color-token? [token]
(= (:type token) :color))

View File

@@ -51,6 +51,7 @@
:ref-shape-is-head
:ref-shape-is-not-head
:shape-ref-in-main
:component-id-mismatch
:root-main-not-allowed
:nested-main-not-allowed
:root-copy-not-allowed
@@ -59,6 +60,7 @@
:not-head-copy-not-allowed
:not-component-not-allowed
:component-nil-objects-not-allowed
:non-deleted-component-cannot-have-objects
:instance-head-not-frame
:invalid-text-touched
:misplaced-slot
@@ -326,6 +328,20 @@
:component-file (:component-file ref-shape)
:component-id (:component-id ref-shape)))))
(defn- check-ref-component-id
"Validate that if the copy has not been swwpped, the component-id and component-file are
the same as in the referenced shape in the near main."
[shape file page libraries]
(when (nil? (ctk/get-swap-slot shape))
(when-let [ref-shape (ctf/find-ref-shape file page libraries shape :include-deleted? true)]
(when (or (not= (:component-id shape) (:component-id ref-shape))
(not= (:component-file shape) (:component-file ref-shape)))
(report-error :component-id-mismatch
"Nested copy component-id and component-file must be the same as the near main"
shape file page
:component-id (:component-id ref-shape)
:component-file (:component-file ref-shape))))))
(defn- check-empty-swap-slot
"Validate that this shape does not have any swap slot."
[shape file page]
@@ -418,6 +434,7 @@
(check-component-not-main-head shape file page libraries)
(check-component-not-root shape file page)
(check-valid-touched shape file page)
(check-ref-component-id shape file page libraries)
;; We can have situations where the nested copy and the ancestor copy come from different libraries and some of them have been dettached
;; so we only validate the shape-ref if the ancestor is from a valid library
(when library-exists
@@ -648,6 +665,13 @@
"Component main not allowed inside other component"
main-instance file component-page))))
(defn- check-not-objects
[component file]
(when (d/not-empty? (:objects component))
(report-error :non-deleted-component-cannot-have-objects
"A non-deleted component cannot have shapes inside"
component file nil)))
(defn- check-component
"Validate semantic coherence of a component. Report all errors found."
[component file]
@@ -656,7 +680,8 @@
"Objects list cannot be nil"
component file nil))
(when-not (:deleted component)
(check-main-inside-main component file))
(check-main-inside-main component file)
(check-not-objects component file))
(when (:deleted component)
(check-component-duplicate-swap-slot component file)
(check-ref-cycles component file))

View File

@@ -1,15 +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.common.i18n
"Dummy i18n functions, to be used by code in common that needs translations.")
(defn tr
"This function will be monkeypatched at runtime with the real function in frontend i18n.
Here it just returns the key passed as argument. This way the result can be used in
unit tests or backend code for logs or error messages."
[key & _args]
key)

View File

@@ -1769,6 +1769,23 @@
(pcb/update-shapes changes [(:id dest-shape)] ctk/unhead-shape {:ignore-touched true})
changes))
(defn- check-swapped-main
[changes dest-shape origin-shape]
;; Only for direct updates (from main to copy). Check if the main shape
;; has been swapped. If so, the new component-id and component-file must
;; be put into the copy.
(if (and (= (:shape-ref dest-shape) (:id origin-shape))
(ctk/instance-head? dest-shape)
(ctk/instance-head? origin-shape)
(or (not= (:component-id dest-shape) (:component-id origin-shape))
(not= (:component-file dest-shape) (:component-file origin-shape))))
(pcb/update-shapes changes [(:id dest-shape)]
#(assoc %
:component-id (:component-id origin-shape)
:component-file (:component-file origin-shape))
{:ignore-touched true})
changes))
(defn- update-attrs
"The main function that implements the attribute sync algorithm. Copy
attributes that have changed in the origin shape to the dest shape.
@@ -1811,6 +1828,8 @@
:always
(check-detached-main dest-shape origin-shape)
:always
(check-swapped-main dest-shape origin-shape)
:always
(generate-update-tokens container dest-shape origin-shape touched omit-touched? nil))
(let [attr-group (get ctk/sync-attrs attr)

View File

@@ -58,7 +58,7 @@
(cto/shape-attr->token-attrs attr changed-sub-attr))]
(if (some #(contains? tokens %) token-attrs)
(pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
(pcb/update-shapes changes [shape-id] #(cto/unapply-token-id % token-attrs))
changes)))
check-shape

View File

@@ -92,16 +92,6 @@
[& items]
(apply mu/merge (map schema items)))
(defn assoc-key
"Add a key & value to a schema"
[s k v]
(mu/assoc (schema s) k v))
(defn dissoc-key
"Remove a key from a schema"
[s k]
(mu/dissoc (schema s) k))
(defn ref?
[s]
(m/-ref-schema? s))
@@ -280,13 +270,6 @@
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options)))))
(defn validation-errors
"Checks a value against a schema. If valid, returns nil. If not, returns a list
of english error messages."
[value schema]
(let [explainer (explainer schema)]
(-> value explainer simplify not-empty)))
(defmacro ignoring
[expr]
(if (:ns &env)

View File

@@ -274,7 +274,7 @@
file-id
{file-id file}
file-id))]
(thf/apply-changes file changes)))
(thf/apply-changes file changes :validate? false)))
(defn swap-component
"Swap the specified shape by the component specified by component-tag"
@@ -305,12 +305,13 @@
[changes nil])
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(when new-shape-label
(thi/rm-id! (:id new-shape))
(thi/set-id! new-shape-label (:id new-shape)))
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))
(defn swap-component-in-shape [file shape-tag component-tag & {:keys [page-label propagate-fn]}]
@@ -339,9 +340,10 @@
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
(:objects page)
{})
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))
(defn update-bottom-color
@@ -357,9 +359,10 @@
(assoc shape :fills (ths/sample-fills-color :fill-color color)))
(:objects page)
{})
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))
(defn reset-overrides [file shape & {:keys [page-label propagate-fn]}]
@@ -374,9 +377,10 @@
{file-id file}
(ctn/make-container container :page)
(:id shape)))
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))
(defn reset-overrides-in-first-child [file shape-tag & {:keys [page-label propagate-fn]}]
@@ -398,9 +402,10 @@
#{(-> (ths/get-shape file shape-tag :page-label page-label)
:id)}
{})
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))
(defn duplicate-shape [file shape-tag & {:keys [page-label propagate-fn]}]
@@ -419,8 +424,9 @@
(:id file)) ;; file-id
(cll/generate-duplicate-changes-update-indices (:objects page) ;; objects
#{(:id shape)}))
file' (thf/apply-changes file changes)]
file' (thf/apply-changes file changes :validate? (not propagate-fn))]
(if propagate-fn
(propagate-fn file')
(-> (propagate-fn file')
(thf/validate-file!))
file')))

View File

@@ -54,12 +54,14 @@
([file] (validate-file! file {}))
([file libraries]
(cfv/validate-file-schema! file)
(cfv/validate-file! file libraries)))
(cfv/validate-file! file libraries)
file))
(defn apply-changes
[file changes]
[file changes & {:keys [validate?] :or {validate? true}}]
(let [file' (ctf/update-file-data file #(cfc/process-changes % (:redo-changes changes) true))]
(validate-file! file')
(when validate?
(validate-file! file'))
file'))
(defn apply-undo-changes

View File

@@ -146,12 +146,15 @@
"Check if some attribute is one that is involved in component syncrhonization.
Note that design tokens also are involved, although they go by an alternate
route and thus they are not part of :sync-attrs.
Also when detaching a nested copy it also needs to trigger a synchronization,
even though :shape-ref is not a synced attribute per se"
Also when detaching a nested copy or it also needs to trigger a synchronization,
even though :shape-ref or :component-id are not synced attribute per se"
[attr]
(or (get sync-attrs attr)
(= :shape-ref attr)
(= :applied-tokens attr)))
(= :applied-tokens attr)
(= :component-id attr)
(= :component-file attr)
(= :component-root attr)))
(defn instance-root?
"Check if this shape is the head of a top instance."

View File

@@ -60,6 +60,9 @@
(some? objects)
(assoc :objects objects)
(nil? objects)
(dissoc :objects)
(some? modified-at)
(assoc :modified-at modified-at)

View File

@@ -9,13 +9,13 @@
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.schema.generators :as sg]
[app.common.time :as ct]
[clojure.data :as data]
[clojure.set :as set]
[cuerdas.core :as str]
[malli.util :as mu]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; GENERAL HELPERS
;; HELPERS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- schema-keys
@@ -45,24 +45,7 @@
[token-name token-value]
(let [token-references (find-token-value-references token-value)
self-reference? (get token-references token-name)]
(boolean self-reference?)))
(defn references-token?
"Recursively check if a value references the token name. Handles strings, maps, and sequences."
[value token-name]
(cond
(string? value)
(boolean (some #(= % token-name) (find-token-value-references value)))
(map? value)
(some true? (map #(references-token? % token-name) (vals value)))
(sequential? value)
(some true? (map #(references-token? % token-name) value))
:else false))
(defn composite-token-reference?
"Predicate if a composite token is a reference value - a string pointing to another token."
[token-value]
(string? token-value))
self-reference?))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA
@@ -71,6 +54,7 @@
(def token-type->dtcg-token-type
{:boolean "boolean"
:border-radius "borderRadius"
:shadow "shadow"
:color "color"
:dimensions "dimension"
:font-family "fontFamilies"
@@ -81,7 +65,6 @@
:opacity "opacity"
:other "other"
:rotation "rotation"
:shadow "shadow"
:sizing "sizing"
:spacing "spacing"
:string "string"
@@ -99,13 +82,14 @@
"boxShadow" :shadow)))
(def composite-token-type->dtcg-token-type
"When converting the type of one element inside a composite token, an additional type
:line-height is available, that is not allowed for a standalone token."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc token-type->dtcg-token-type
:line-height "lineHeights"))
(def composite-dtcg-token-type->token-type
"Same as above, in the opposite direction."
"Custom set of conversion keys for composite typography token with `:line-height` available.
(Penpot doesn't support `:line-height` token)"
(assoc dtcg-token-type->token-type
"lineHeights" :line-height
"lineHeight" :line-height))
@@ -113,111 +97,83 @@
(def token-types
(into #{} (keys token-type->dtcg-token-type)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def schema:token-name
"A token name can contains letters, numbers, underscores the character $ and dots, but
not start with $ or end with a dot. The $ character does not have any special meaning,
but dots separate token groups (e.g. color.primary.background)."
[:re {:title "TokenName"
:gen/gen sg/text}
(def token-name-ref
[:re {:title "TokenNameRef" :gen/gen sg/text}
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)
(dtcg-token-type->token-type type)
type))}
token-types])
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name schema:token-name]
[:type schema:token-type]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SCHEMA: Token application to shape
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; All the following schemas define the `:applied-tokens` attribute of a shape.
;; This attribute is a map <token-attribute> -> <token-name>.
;; Token attributes approximately match shape attributes, but not always.
;; For each schema there is a `*keys` set including all the possible token attributes
;; to which a token of the corresponding type can be applied.
;; Some token types can be applied to some attributes only if the shape has a
;; particular condition (i.e. has a layout itself or is a layout item).
(def ^:private schema:border-radius
[:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} schema:token-name]
[:r2 {:optional true} schema:token-name]
[:r3 {:optional true} schema:token-name]
[:r4 {:optional true} schema:token-name]])
(def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:color
[:map
[:fill {:optional true} schema:token-name]
[:stroke-color {:optional true} schema:token-name]])
[:fill {:optional true} token-name-ref]
[:stroke-color {:optional true} token-name-ref]])
(def color-keys (schema-keys schema:color))
(def ^:private schema:border-radius
[:map {:title "BorderRadiusTokenAttrs"}
[:r1 {:optional true} token-name-ref]
[:r2 {:optional true} token-name-ref]
[:r3 {:optional true} token-name-ref]
[:r4 {:optional true} token-name-ref]])
(def border-radius-keys (schema-keys schema:border-radius))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} token-name-ref]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} token-name-ref]])
(def stroke-width-keys (schema-keys schema:stroke-width))
(def ^:private schema:sizing-base
[:map {:title "SizingBaseTokenAttrs"}
[:width {:optional true} schema:token-name]
[:height {:optional true} schema:token-name]])
[:width {:optional true} token-name-ref]
[:height {:optional true} token-name-ref]])
(def ^:private schema:sizing-layout-item
[:map {:title "SizingLayoutItemTokenAttrs"}
[:layout-item-min-w {:optional true} schema:token-name]
[:layout-item-max-w {:optional true} schema:token-name]
[:layout-item-min-h {:optional true} schema:token-name]
[:layout-item-max-h {:optional true} schema:token-name]])
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
[:layout-item-min-w {:optional true} token-name-ref]
[:layout-item-max-w {:optional true} token-name-ref]
[:layout-item-min-h {:optional true} token-name-ref]
[:layout-item-max-h {:optional true} token-name-ref]])
(def ^:private schema:sizing
(-> (reduce mu/union [schema:sizing-base
schema:sizing-layout-item])
(mu/update-properties assoc :title "SizingTokenAttrs")))
(def sizing-layout-item-keys (schema-keys schema:sizing-layout-item))
(def sizing-keys (schema-keys schema:sizing))
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} token-name-ref]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:spacing-gap
[:map {:title "SpacingGapTokenAttrs"}
[:row-gap {:optional true} schema:token-name]
[:column-gap {:optional true} schema:token-name]])
[:row-gap {:optional true} token-name-ref]
[:column-gap {:optional true} token-name-ref]])
(def ^:private schema:spacing-padding
[:map {:title "SpacingPaddingTokenAttrs"}
[:p1 {:optional true} schema:token-name]
[:p2 {:optional true} schema:token-name]
[:p3 {:optional true} schema:token-name]
[:p4 {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
[:p1 {:optional true} token-name-ref]
[:p2 {:optional true} token-name-ref]
[:p3 {:optional true} token-name-ref]
[:p4 {:optional true} token-name-ref]])
(def ^:private schema:spacing-margin
[:map {:title "SpacingMarginTokenAttrs"}
[:m1 {:optional true} schema:token-name]
[:m2 {:optional true} schema:token-name]
[:m3 {:optional true} schema:token-name]
[:m4 {:optional true} schema:token-name]])
(def spacing-margin-keys (schema-keys schema:spacing-margin))
[:m1 {:optional true} token-name-ref]
[:m2 {:optional true} token-name-ref]
[:m3 {:optional true} token-name-ref]
[:m4 {:optional true} token-name-ref]])
(def ^:private schema:spacing
(-> (reduce mu/union [schema:spacing-gap
@@ -225,13 +181,16 @@
schema:spacing-margin])
(mu/update-properties assoc :title "SpacingTokenAttrs")))
(def spacing-margin-keys (schema-keys schema:spacing-margin))
(def spacing-keys (schema-keys schema:spacing))
(def ^:private schema:stroke-width
[:map
[:stroke-width {:optional true} schema:token-name]])
(def ^:private schema:spacing-gap-padding
(-> (reduce mu/union [schema:spacing-gap
schema:spacing-padding])
(mu/update-properties assoc :title "SpacingGapPaddingTokenAttrs")))
(def stroke-width-keys (schema-keys schema:stroke-width))
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
(def ^:private schema:dimensions
(-> (reduce mu/union [schema:sizing
@@ -242,109 +201,91 @@
(def dimensions-keys (schema-keys schema:dimensions))
(def ^:private schema:font-family
(def ^:private schema:axis
[:map
[:font-family {:optional true} schema:token-name]])
[:x {:optional true} token-name-ref]
[:y {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} schema:token-name]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} schema:token-name]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} schema:token-name]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:line-height ;; This is not available for standalone tokens, only typography
[:map {:title "LineHeightTokenAttrs"}
[:line-height {:optional true} schema:token-name]])
(def line-height-keys (schema-keys schema:line-height))
(def axis-keys (schema-keys schema:axis))
(def ^:private schema:rotation
[:map {:title "RotationTokenAttrs"}
[:rotation {:optional true} schema:token-name]])
[:rotation {:optional true} token-name-ref]])
(def rotation-keys (schema-keys schema:rotation))
(def ^:private schema:font-size
[:map {:title "FontSizeTokenAttrs"}
[:font-size {:optional true} token-name-ref]])
(def font-size-keys (schema-keys schema:font-size))
(def ^:private schema:letter-spacing
[:map {:title "LetterSpacingTokenAttrs"}
[:letter-spacing {:optional true} token-name-ref]])
(def letter-spacing-keys (schema-keys schema:letter-spacing))
(def ^:private schema:font-family
[:map
[:font-family {:optional true} token-name-ref]])
(def font-family-keys (schema-keys schema:font-family))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} token-name-ref]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:font-weight
[:map
[:font-weight {:optional true} token-name-ref]])
(def font-weight-keys (schema-keys schema:font-weight))
(def ^:private schema:typography
[:map
[:typography {:optional true} token-name-ref]])
(def typography-token-keys (schema-keys schema:typography))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} token-name-ref]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def typography-keys (set/union font-size-keys
letter-spacing-keys
font-family-keys
font-weight-keys
text-case-keys
text-decoration-keys
font-weight-keys
typography-token-keys
#{:line-height}))
(def ^:private schema:number
(-> (reduce mu/union [schema:line-height
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
schema:rotation])
(mu/update-properties assoc :title "NumberTokenAttrs")))
(def number-keys (schema-keys schema:number))
(def ^:private schema:opacity
[:map {:title "OpacityTokenAttrs"}
[:opacity {:optional true} schema:token-name]])
(def opacity-keys (schema-keys schema:opacity))
(def ^:private schema:shadow
[:map {:title "ShadowTokenAttrs"}
[:shadow {:optional true} schema:token-name]])
(def shadow-keys (schema-keys schema:shadow))
(def ^:private schema:text-case
[:map
[:text-case {:optional true} schema:token-name]])
(def text-case-keys (schema-keys schema:text-case))
(def ^:private schema:text-decoration
[:map
[:text-decoration {:optional true} schema:token-name]])
(def text-decoration-keys (schema-keys schema:text-decoration))
(def ^:private schema:typography
[:map
[:typography {:optional true} schema:token-name]])
(def typography-token-keys (schema-keys schema:typography))
(def typography-keys (set/union font-family-keys
font-size-keys
font-weight-keys
font-weight-keys
letter-spacing-keys
line-height-keys
text-case-keys
text-decoration-keys
typography-token-keys))
(def ^:private schema:axis
[:map
[:x {:optional true} schema:token-name]
[:y {:optional true} schema:token-name]])
(def axis-keys (schema-keys schema:axis))
(def all-keys (set/union axis-keys
(def all-keys (set/union color-keys
border-radius-keys
color-keys
dimensions-keys
number-keys
opacity-keys
rotation-keys
shadow-keys
sizing-keys
spacing-keys
stroke-width-keys
sizing-keys
opacity-keys
spacing-keys
dimensions-keys
axis-keys
rotation-keys
typography-keys
typography-token-keys))
typography-token-keys
number-keys))
(def ^:private schema:tokens
[:map {:title "GenericTokenAttrs"}])
@@ -365,28 +306,11 @@
schema:text-decoration
schema:dimensions])
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by token type
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn token-attr?
[attr]
(contains? all-keys attr))
(defn token-attr->shape-attr
"Returns the actual shape attribute affected when a token have been applied
to a given `token-attr`."
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
(defn shape-attr->token-attrs
"Returns the token-attr affected when a given attribute in a shape is changed.
The sub-attr is for attributes that may have multiple values, like strokes
(may be width or color) and layout padding & margin (may have 4 edges)."
([shape-attr] (shape-attr->token-attrs shape-attr nil))
([shape-attr changed-sub-attr]
(cond
@@ -428,13 +352,21 @@
(number-keys shape-attr) #{shape-attr}
(axis-keys shape-attr) #{shape-attr})))
(defn token-attr->shape-attr
[token-attr]
(case token-attr
:fill :fills
:stroke-color :strokes
:stroke-width :strokes
token-attr))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for token attributes by shape type
;; TOKEN SHAPE ATTRIBUTES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def ^:private position-attributes #{:x :y})
(def position-attributes #{:x :y})
(def ^:private generic-attributes
(def generic-attributes
(set/union color-keys
stroke-width-keys
rotation-keys
@@ -443,22 +375,20 @@
shadow-keys
position-attributes))
(def ^:private rect-attributes
(def rect-attributes
(set/union generic-attributes
border-radius-keys))
(def ^:private frame-with-layout-attributes
(def frame-with-layout-attributes
(set/union rect-attributes
spacing-gap-padding-keys))
(def ^:private text-attributes
(def text-attributes
(set/union generic-attributes
typography-keys
number-keys))
(defn shape-type->attributes
"Returns what token attributes may be applied to a shape depending on its type
and if it is a frame with a layout."
[type is-layout]
(case type
:bool generic-attributes
@@ -474,14 +404,12 @@
nil))
(defn appliable-attrs-for-shape
"Returns which ones of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Returns intersection of shape `attributes` for `shape-type`."
[attributes shape-type is-layout]
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
(defn any-appliable-attr-for-shape?
"Returns if any of the given `attributes` can be applied to a shape
of type `shape-type` and `is-layout`."
"Checks if `token-type` supports given shape `attributes`."
[attributes token-type is-layout]
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
@@ -492,6 +420,42 @@
typography-keys
#{:fill}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKENS IN SHAPES
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- toggle-or-apply-token
"Remove any shape attributes from token if they exists.
Othewise apply token attributes."
[shape token]
(let [[shape-leftover token-leftover _matching] (data/diff (:applied-tokens shape) token)]
(merge {} shape-leftover token-leftover)))
(defn- token-from-attributes [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn- apply-token-to-attributes [{:keys [shape token attributes]}]
(let [token (token-from-attributes token attributes)]
(toggle-or-apply-token shape token)))
(defn apply-token-to-shape
[{:keys [shape token attributes] :as _props}]
(let [applied-tokens (apply-token-to-attributes {:shape shape
:token token
:attributes attributes})]
(update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-token-id [shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-token-id shape layout-item-attrs)))
(def tokens-by-input
"A map from input name to applicable token for that input."
{:width #{:sizing :dimensions}
@@ -517,48 +481,7 @@
:stroke-color #{:color}})
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for tokens application
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TODO it seems that this function is redundant, maybe?
;; (defn- toggle-or-apply-token
;; "Remove any shape attributes from token if they exists.
;; Othewise apply token attributes."
;; [shape token]
;; (let [[only-in-shape only-in-token _matching] (data/diff (:applied-tokens shape) token)]
;; (merge {} only-in-shape only-in-token)))
(defn- generate-attr-map [token attributes]
(->> (map (fn [attr] [attr (:name token)]) attributes)
(into {})))
(defn apply-token-to-shape
"Applies the token to the given attributes in the shape."
[{:keys [shape token attributes] :as _props}]
(let [map-to-apply (generate-attr-map token attributes)]
(update shape :applied-tokens #(merge % map-to-apply))))
;; (defn apply-token-to-shape
;; [{:keys [shape token attributes] :as _props}]
;; (let [map-to-apply (generate-attr-map token attributes)
;; applied-tokens (toggle-or-apply-token shape map-to-apply)]
;; (update shape :applied-tokens #(merge % applied-tokens))))
(defn unapply-tokens-from-shape
"Removes any token applied to the given attributes in the shape."
[shape attributes]
(update shape :applied-tokens d/without-keys attributes))
(defn unapply-layout-item-tokens
"Unapplies all layout item related tokens from shape."
[shape]
(let [layout-item-attrs (set/union sizing-layout-item-keys
spacing-margin-keys)]
(unapply-tokens-from-shape shape layout-item-attrs)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; HELPERS for typography tokens
;; TYPOGRAPHY
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn split-font-family
@@ -622,7 +545,6 @@
(cond-> {:weight weight}
italic? (assoc :style "italic")))))
;; review this
(defn typography-composite-token-reference?
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
[token-value]
@@ -636,19 +558,3 @@
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
[token-value]
(string? token-value))
(defn update-token-value-references
"Recursively update token references within a token value, supporting complex token values (maps, sequences, strings)."
[value old-name new-name]
(cond
(string? value)
(str/replace value
(re-pattern (str "\\{" (str/replace old-name "." "\\.") "\\}"))
(str "{" new-name "}"))
(map? value)
(d/update-vals value #(update-token-value-references % old-name new-name))
(sequential? value)
(mapv #(update-token-value-references % old-name new-name) value)
:else
value))
;; end review this

View File

@@ -114,19 +114,25 @@
[o]
(instance? Token o))
(def schema:token-attrs
[:map {:title "Token"}
[:id ::sm/uuid]
[:name cto/token-name-ref]
[:type [::sm/one-of cto/token-types]]
[:value ::sm/any]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]])
(declare make-token)
(def schema:token
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
[:and {:gen/gen (->> (sg/generator schema:token-attrs)
(sg/fmap #(make-token %)))}
(sm/required-keys cto/schema:token-attrs)
(sm/required-keys schema:token-attrs)
[:fn token?]])
(def ^:private check-token-attrs
(sm/check-fn cto/schema:token-attrs :hint "expected valid params for token"))
(def decode-token-attrs
(sm/lazy-decoder cto/schema:token-attrs sm/json-transformer))
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
(def check-token
(sm/check-fn schema:token :hint "expected valid token"))
@@ -311,13 +317,10 @@
[o]
(instance? TokenSetLegacy o))
(declare make-token-set)
(declare normalized-set-name?)
(def schema:token-set-attrs
[:map {:title "TokenSet"}
[:id ::sm/uuid]
[:name [:and :string [:fn #(normalized-set-name? %)]]]
[:name :string]
[:description {:optional true} :string]
[:modified-at {:optional true} ::ct/inst]
[:tokens {:optional true
@@ -339,6 +342,8 @@
:string schema:token]
[:fn d/ordered-map?]]]])
(declare make-token-set)
(def schema:token-set
[:schema {:gen/gen (->> (sg/generator schema:token-set-attrs)
(sg/fmap #(make-token-set %)))}
@@ -399,25 +404,12 @@
(split-set-name name))
(cpn/join-path :separator set-separator :with-spaces? false))))
(defn normalized-set-name?
"Check if a set name is normalized (no extra spaces)."
[name]
(= name (normalize-set-name name)))
(defn replace-last-path-name
"Replaces the last element in a `path` vector with `name`."
[path name]
(-> (into [] (drop-last path))
(conj name)))
(defn make-child-name
"Generate the name of a set child of `parent-set` adding the name `name`."
[parent-set name]
(if-let [parent-path (get-set-path parent-set)]
(->> (concat parent-path (split-set-name name))
(join-set-path))
(normalize-set-name name)))
;; The following functions will be removed after refactoring the internal structure of TokensLib,
;; since we'll no longer need group prefixes to differentiate between sets and set-groups.
@@ -917,8 +909,7 @@ Will return a value that matches this schema:
`:all` All of the nested sets are active
`:partial` Mixed active state of nested sets")
(get-tokens-in-active-sets [_] "set of set names that are active in the the active themes")
(get-all-tokens [_] "all tokens in the lib, as a sequence")
(get-all-tokens-map [_] "all tokens in the lib, as a map name -> token")
(get-all-tokens [_] "all tokens in the lib")
(get-tokens [_ set-id] "return a map of tokens in the set, indexed by token-name"))
(declare parse-multi-set-dtcg-json)
@@ -1315,10 +1306,6 @@ Will return a value that matches this schema:
tokens))
(get-all-tokens [this]
(mapcat #(vals (get-tokens- %))
(get-sets this)))
(get-all-tokens-map [this]
(reduce
(fn [tokens' set]
(into tokens' (map (fn [x] [(:name x) x]) (vals (get-tokens- set)))))
@@ -1378,13 +1365,10 @@ Will return a value that matches this schema:
(def ^:private check-tokens-lib-map
(sm/check-fn schema:tokens-lib-map :hint "invalid tokens-lib internal data structure"))
(defn tokens-lib?
[o]
(instance? TokensLib o))
(defn valid-tokens-lib?
[o]
(and (tokens-lib? o) (valid? o)))
(and (instance? TokensLib o)
(valid? o)))
(defn- ensure-hidden-theme
"A helper that is responsible to ensure that the hidden theme always
@@ -1446,50 +1430,6 @@ Will return a value that matches this schema:
(rename copy-name)
(reid (uuid/next))))))
(defn- token-name->path-selector
"Splits token-name into map with `:path` and `:selector` using `token-name->path`.
`:selector` is the last item of the names path
`:path` is everything leading up the the `:selector`."
[token-name]
(let [path-segments (get-token-path {:name token-name})
last-idx (dec (count path-segments))
[path [selector]] (split-at last-idx path-segments)]
{:path (seq path)
:selector selector}))
(defn token-name-path-exists?
"Traverses the path from `token-name` down a `tokens-tree` and checks if a token at that path exists.
It's not allowed to create a token inside a token. E.g.:
Creating a token with
{:name \"foo.bar\"}
in the tokens tree:
{\"foo\" {:name \"other\"}}"
[token-name tokens-tree]
(let [{:keys [path selector]} (token-name->path-selector token-name)
path-target (reduce
(fn [acc cur]
(let [target (get acc cur)]
(cond
;; Path segment doesn't exist yet
(nil? target) (reduced false)
;; A token exists at this path
(:name target) (reduced true)
;; Continue traversing the true
:else target)))
tokens-tree
path)]
(cond
(boolean? path-target) path-target
(get path-target :name) true
:else (-> (get path-target selector)
(seq)
(boolean)))))
;; === Import / Export from JSON format
;; Supported formats:

View File

@@ -6,34 +6,34 @@
(ns common-tests.files.tokens-test
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[clojure.test :as t]))
(t/deftest test-parse-token-value
(t/testing "parses double from a token value"
(t/is (= {:value 100.1 :unit nil} (cfo/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cfo/parse-token-value "-9"))))
(t/is (= {:value 100.1 :unit nil} (cft/parse-token-value "100.1")))
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
(t/testing "trims white-space"
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
(t/testing "parses unit: px"
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
(t/testing "parses unit: %"
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
(t/testing "parses unit: px")
(t/testing "returns nil for any invalid characters"
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
(t/is (nil? (cft/parse-token-value " -1.3a "))))
(t/testing "doesnt accept invalid double"
(t/is (nil? (cfo/parse-token-value ".3")))))
(t/is (nil? (cft/parse-token-value ".3")))))
(t/deftest token-applied-test
(t/testing "matches passed token with `:token-attributes`"
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match empty token"
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "does't match passed token `:id`"
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
(t/testing "doesn't match passed `:token-attributes`"
(t/is (nil? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
(t/deftest shapes-ids-by-applied-attributes
(t/testing "Returns set of matched attributes that fit the applied token"
@@ -54,7 +54,7 @@
shape-applied-x-y
shape-applied-all
shape-applied-none]
expected (cfo/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
expected (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
(t/is (= (:x expected) (shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all)))
@@ -62,21 +62,34 @@
shape-applied-x-y
shape-applied-all)))
(t/is (= (:z expected) (shape-ids shape-applied-all)))
(t/is (true? (cfo/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cfo/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(t/is (true? (cft/shapes-applied-all? expected (shape-ids shape-applied-all) attributes)))
(t/is (false? (cft/shapes-applied-all? expected (apply shape-ids shapes) attributes)))
(shape-ids shape-applied-x
shape-applied-x-y
shape-applied-all))))
(t/deftest tokens-applied-test
(t/testing "is true when single shape matches the token and attributes"
(t/is (true? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (true? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "b"}}]
#{:x}))))
(t/testing "is false when no shape matches the token or attributes"
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
{:applied-tokens {:x "b"}}]
#{:x})))
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
{:applied-tokens {:x "a"}}]
#{:y})))))
(t/deftest name->path-test
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo.bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz")))
(t/is (= ["foo" "bar" "baz"] (cft/token-name->path "foo..bar.baz...."))))
(t/deftest token-name-path-exists?-test
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (cft/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (cft/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (cft/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -465,9 +465,10 @@
page
{(:id file) file}
(thi/id :nested-h-ellipse))
file' (-> (thf/apply-changes file changes)
file' (-> (thf/apply-changes file changes :validate? false)
(tho/propagate-component-changes :c-board-with-ellipse)
(tho/propagate-component-changes :c-big-board))
(tho/propagate-component-changes :c-big-board)
(thf/validate-file!))
;; ==== Get
nested2-h-ellipse (ths/get-shape file' :nested-h-ellipse)

View File

@@ -64,9 +64,8 @@
(reset-all-overrides [file]
(-> file
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1)
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2)
(propagate-all-component-changes)))
(tho/reset-overrides-in-first-child :frame-board-1 :page-label :page-1 :propagate-fn propagate-all-component-changes)
(tho/reset-overrides-in-first-child :copy-board-1 :page-label :page-2 :propagate-fn propagate-all-component-changes)))
(fill-colors [file]
[(tho/bottom-fill-color file :frame-ellipse-1 :page-label :page-1)

View File

@@ -56,10 +56,9 @@
(reset-all-overrides [file]
(-> file
(tho/reset-overrides (ths/get-shape file :copy-simple-1))
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1))
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy))
(propagate-all-component-changes)))
(tho/reset-overrides (ths/get-shape file :copy-simple-1 :propagate-fn propagate-all-component-changes))
(tho/reset-overrides (ths/get-shape file :copy-frame-composed-1 :propagate-fn propagate-all-component-changes))
(tho/reset-overrides (ths/get-shape file :composed-1-composed-2-copy :propagate-fn propagate-all-component-changes))))
(fill-colors [file]
[(tho/bottom-fill-color file :frame-simple-1)

View File

@@ -6,20 +6,12 @@
(ns common-tests.logic.swap-as-override-test
(:require
[app.common.files.changes :as ch]
[app.common.files.changes-builder :as pcb]
[app.common.logic.libraries :as cll]
[app.common.logic.shapes :as cls]
[app.common.pprint :as pp]
[app.common.data :as d]
[app.common.test-helpers.components :as thc]
[app.common.test-helpers.compositions :as tho]
[app.common.test-helpers.files :as thf]
[app.common.test-helpers.ids-map :as thi]
[app.common.test-helpers.shapes :as ths]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
[app.common.types.file :as ctf]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/use-fixtures :each thi/test-fixture)
@@ -27,23 +19,40 @@
(defn- setup []
(-> (thf/sample-file :file1)
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1 :child-params {:name "child-component-1" :type :rect :fills (ths/sample-fills-color :fill-color "#111111")})
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2 :child-params {:name "child-component-2" :type :rect :fills (ths/sample-fills-color :fill-color "#222222")})
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3 :child-params {:name "child-component-3" :type :rect :fills (ths/sample-fills-color :fill-color "#333333")})
(tho/add-simple-component :component-1 :frame-component-1 :child-component-1
:root-params {:name "component-1"}
:child-params {:name "child-component-1"
:type :rect
:fills (ths/sample-fills-color :fill-color "#111111")})
(tho/add-simple-component :component-2 :frame-component-2 :child-component-2
:root-params {:name "component-2"}
:child-params {:name "child-component-2"
:type :rect
:fills (ths/sample-fills-color :fill-color "#222222")})
(tho/add-simple-component :component-3 :frame-component-3 :child-component-3
:root-params {:name "component-3"}
:child-params {:name "child-component-3"
:type :rect
:fills (ths/sample-fills-color :fill-color "#333333")})
(tho/add-frame :frame-icon-and-text)
(thc/instantiate-component :component-1 :copy-component-1 :parent-label :frame-icon-and-text :children-labels [:component-1-icon-and-text])
(tho/add-frame :frame-icon-and-text :name "copy-component-1")
(thc/instantiate-component :component-1 :copy-component-1
:parent-label :frame-icon-and-text
:children-labels [:component-1-icon-and-text])
(ths/add-sample-shape :text
{:type :text
:name "icon+text"
:parent-label :frame-icon-and-text})
(thc/make-component :icon-and-text :frame-icon-and-text)
(tho/add-frame :frame-panel)
(thc/instantiate-component :icon-and-text :copy-icon-and-text :parent-label :frame-panel :children-labels [:icon-and-text-panel])
(tho/add-frame :frame-panel :name "icon-and-text")
(thc/instantiate-component :icon-and-text :copy-icon-and-text
:parent-label :frame-panel
:children-labels [:icon-and-text-panel])
(thc/make-component :panel :frame-panel)
(thc/instantiate-component :panel :copy-panel :children-labels [:copy-icon-and-text-panel])))
(thc/instantiate-component :panel :copy-panel
:children-labels [:copy-icon-and-text-panel])))
(defn- propagate-all-component-changes [file]
(-> file

View File

@@ -255,28 +255,28 @@
(cls/generate-update-shapes [(:id frame1)]
(fn [shape]
(-> shape
(cto/unapply-tokens-from-shape [:r1 :r2 :r3 :r4])
(cto/unapply-tokens-from-shape [:rotation])
(cto/unapply-tokens-from-shape [:opacity])
(cto/unapply-tokens-from-shape [:stroke-width])
(cto/unapply-tokens-from-shape [:stroke-color])
(cto/unapply-tokens-from-shape [:fill])
(cto/unapply-tokens-from-shape [:width :height])))
(cto/unapply-token-id [:r1 :r2 :r3 :r4])
(cto/unapply-token-id [:rotation])
(cto/unapply-token-id [:opacity])
(cto/unapply-token-id [:stroke-width])
(cto/unapply-token-id [:stroke-color])
(cto/unapply-token-id [:fill])
(cto/unapply-token-id [:width :height])))
(:objects page)
{})
(cls/generate-update-shapes [(:id text1)]
(fn [shape]
(-> shape
(cto/unapply-tokens-from-shape [:font-size])
(cto/unapply-tokens-from-shape [:letter-spacing])
(cto/unapply-tokens-from-shape [:font-family])))
(cto/unapply-token-id [:font-size])
(cto/unapply-token-id [:letter-spacing])
(cto/unapply-token-id [:font-family])))
(:objects page)
{})
(cls/generate-update-shapes [(:id circle1)]
(fn [shape]
(-> shape
(cto/unapply-tokens-from-shape [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-tokens-from-shape [:m1 :m2 :m3 :m4])))
(cto/unapply-token-id [:layout-item-max-h :layout-item-min-h :layout-item-max-w :layout-item-min-w])
(cto/unapply-token-id [:m1 :m2 :m3 :m4])))
(:objects page)
{}))

View File

@@ -8,19 +8,20 @@
(:require
[app.common.schema :as sm]
[app.common.types.token :as cto]
[app.common.uuid :as uuid]
[clojure.test :as t]))
(t/deftest test-valid-token-name-schema
;; Allow regular namespace token names
(t/is (true? (sm/validate cto/schema:token-name "Foo")))
(t/is (true? (sm/validate cto/schema:token-name "foo")))
(t/is (true? (sm/validate cto/schema:token-name "FOO")))
(t/is (true? (sm/validate cto/schema:token-name "Foo.Bar.Baz")))
(t/is (true? (sm/validate cto/token-name-ref "Foo")))
(t/is (true? (sm/validate cto/token-name-ref "foo")))
(t/is (true? (sm/validate cto/token-name-ref "FOO")))
(t/is (true? (sm/validate cto/token-name-ref "Foo.Bar.Baz")))
;; Disallow trailing tokens
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
;; Disallow multiple separator dots
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
;; Disallow any special characters
(t/is (false? (sm/validate cto/schema:token-name "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/schema:token-name "Hey%Foo.Bar"))))
(t/is (false? (sm/validate cto/token-name-ref "Hey Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey😈Foo.Bar")))
(t/is (false? (sm/validate cto/token-name-ref "Hey%Foo.Bar"))))

View File

@@ -678,35 +678,35 @@
(t/deftest list-active-themes-tokens-bug-taiga-10617
(let [tokens-lib (-> (ctob/make-tokens-lib)
(ctob/add-set (ctob/make-token-set :name "Mode/Dark"
(ctob/add-set (ctob/make-token-set :name "Mode / Dark"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#700000")}))
(ctob/add-set (ctob/make-token-set :name "Mode/Light"
(ctob/add-set (ctob/make-token-set :name "Mode / Light"
:tokens {"red"
(ctob/make-token :name "red"
:type :color
:value "#ff0000")}))
(ctob/add-set (ctob/make-token-set :name "Device/Desktop"
(ctob/add-set (ctob/make-token-set :name "Device / Desktop"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 30)}))
(ctob/add-set (ctob/make-token-set :name "Device/Mobile"
(ctob/add-set (ctob/make-token-set :name "Device / Mobile"
:tokens {"border1"
(ctob/make-token :name "border1"
:type :border-radius
:value 50)}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Mobile"
:sets #{"Mode/Dark" "Device/Mobile"}))
:sets #{"Mode / Dark" "Device / Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "App"
:name "Web"
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop"}))
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand A"
:sets #{"Mode/Dark" "Mode/Light" "Device/Desktop" "Device/Mobile"}))
:sets #{"Mode / Dark" "Mode / Light" "Device / Desktop" "Device / Mobile"}))
(ctob/add-theme (ctob/make-token-theme :group "Brand"
:name "Brand B"
:sets #{}))
@@ -2013,11 +2013,3 @@
(t/is (some? imported-ref))
(t/is (= (:type original-ref) (:type imported-ref)))
(t/is (= (:value imported-ref) (:value original-ref))))))))
(t/deftest token-name-path-exists?-test
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {"sm" {:name "sm"}}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm" {"border-radius" {:name "sm"}})))
(t/is (true? (ctob/token-name-path-exists? "border-radius.sm.x" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "other" {"border-radius" {:name "sm"}})))
(t/is (false? (ctob/token-name-path-exists? "dark.border-radius.md" {"dark" {"border-radius" {"sm" {:name "sm"}}}}))))

View File

@@ -41,10 +41,7 @@ services:
- 6062:6062
- 6063:6063
- 6064:6064
- 9000:9000
- 9001:9001
- 9090:9090
- 9091:9091
environment:
- EXTERNAL_UID=${CURRENT_USER_ID}

View File

@@ -145,8 +145,8 @@ http {
proxy_pass http://127.0.0.1:3000/;
}
location /wasm-playground {
alias /home/penpot/penpot/frontend/resources/public/wasm-playground/;
location /playground {
alias /home/penpot/penpot/experiments/;
add_header Cache-Control "no-cache, max-age=0";
autoindex on;
}

View File

@@ -198,10 +198,10 @@ export class WorkspacePage extends BaseWebSocketPage {
`[id="shape-00000000-0000-0000-0000-000000000000"]`,
);
this.toolbarOptions = page.getByTestId("toolbar-options");
this.rectShapeButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Rectangle" });
this.ellipseShapeButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Ellipse" });
this.moveButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Move" });
this.boardButton = page.getByTestId("toolbar-options").getByRole("button", { name: "Board" });
this.rectShapeButton = page.getByRole("button", { name: "Rectangle (R)" });
this.ellipseShapeButton = page.getByRole("button", { name: "Ellipse (E)" });
this.moveButton = page.getByRole("button", { name: "Move (V)" });
this.boardButton = page.getByRole("button", { name: "Board (B)" });
this.toggleToolbarButton = page.getByRole("button", {
name: "Toggle toolbar",
});

View File

@@ -20,7 +20,12 @@ test.describe("Dashboard Deleted Page", () => {
// Navigate directly to deleted page
await dashboardPage.goToDeleted();
// Check for the delete-page-section element
await expect(page.getByTestId("deleted-page-section")).toBeVisible();
// Check for the restore all and clear trash buttons
await expect(
page.getByRole("button", { name: "Restore All" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Clear trash" }),
).toBeVisible();
});
});

View File

@@ -189,8 +189,8 @@ test("BUG 7760 - Layout losing properties when changing parents", async ({
await workspacePage.clickLeafLayer("Flex Board");
// Move the first board into the second
const hAuto = await workspacePage.page.getByTestId("behaviour-h-auto");
const vAuto = await workspacePage.page.getByTestId("behaviour-v-auto");
const hAuto = await workspacePage.page.getByTitle("Fit content (Horizontal)");
const vAuto = await workspacePage.page.getByTitle("Fit content (Vertical)");
await expect(vAuto.locator("input")).toBeChecked();
await expect(hAuto.locator("input")).toBeChecked();

View File

@@ -2418,12 +2418,10 @@ test.describe("Tokens: Apply token", () => {
await nameField.fill(newTokenTitle);
const referenceTabButton =
tokensUpdateCreateModal.getByRole('button', { name: 'Use a reference' });
tokensUpdateCreateModal.getByTestId("reference-opt");
referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByRole('textbox', {
name: 'Reference'
});
const referenceField = tokensUpdateCreateModal.getByLabel("Reference");
await referenceField.fill("{Full}");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
@@ -2742,626 +2740,3 @@ test.describe("Tokens: Apply token", () => {
});
});
});
test.describe("Tokens: Remapping Feature", () => {
test.describe("Box Shadow Token Remapping", () => {
test("User renames box shadow token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-shadow");
const colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base-shadow
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"});
await nameField.fill("derived-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"});
await referenceField.fill("{base-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base-shadow token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-shadow",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("foundation-shadow");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
await expect(remappingModal).toContainText("1");
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "foundation-shadow" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "derived-shadow" }),
).toBeVisible();
});
test("User renames and updates shadow token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base shadow token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-shadow");
let colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#000000");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived shadow token that references base
await tokensTabPanel
.getByRole("button", { name: "Add Token: Shadow" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-shadow");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"});
await referenceField.fill("{primary-shadow}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const cardShadowToken = tokensSidebar.getByRole("button", {
name: "card-shadow",
});
await cardShadowToken.click();
// Rename and update value of base token
const primaryToken = tokensSidebar.getByRole("button", {
name: "primary-shadow",
});
await primaryToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("main-shadow");
// Update the color value
colorField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Color",
});
await colorField.fill("#FF0000");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "main-shadow" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "card-shadow" }),
).toBeVisible();
// Verify the shape still has the token applied with the NEW name
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Button" })
.click();
// Verify the shape still has the shadow applied with the UPDATED color value
// Expand the shadow section to access the color field
const shadowSection = workspacePage.rightSidebar.getByTestId("shadow-section");
await expect(shadowSection).toBeVisible();
// Click to expand the shadow options (the menu button)
const shadowMenuButton = shadowSection
.getByRole("button", { name: "options" })
.first();
await shadowMenuButton.click();
// Wait for the advanced options to appear
await page.waitForTimeout(500);
// Verify the color value has updated from #000000 to #FF0000
const colorInput = shadowSection.getByRole("textbox", { name: "Color" });
expect(colorInput).not.toBeNull();
const colorValue = await colorInput.inputValue();
expect(colorValue.toUpperCase()).toBe("FF0000");
});
});
test.describe("Typography Token Remapping", () => {
test("User renames typography token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-text");
const fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"});
await nameField.fill("body-text");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"})
await referenceField.fill("{base-text}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-text",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("default-text");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "default-text" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "body-text" }),
).toBeVisible();
});
test("User renames and updates typography token - referenced token and applied shapes update", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("body-style");
let fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("16");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived typography token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByRole("textbox", {name: "Name"});
await nameField.fill("paragraph-style");
const referenceToggle =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceToggle.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {name: "Reference"});
await referenceField.fill("{body-style}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Apply the referenced token to a text shape
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
await page.getByRole("tab", { name: "Tokens" }).click();
const paragraphToken = tokensSidebar.getByRole("button", {
name: "paragraph-style",
});
await paragraphToken.click();
// Rename and update value of base token
const bodyToken = tokensSidebar.getByRole("button", {
name: "body-style",
});
await bodyToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("text-base");
// Update the font size value
fontSizeField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Font size",
});
await fontSizeField.fill("18");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "text-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "paragraph-style" }),
).toBeVisible();
// Verify the text shape still has the token applied with NEW name and value
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers
.getByTestId("layer-row")
.filter({ hasText: "Some Text" })
.click();
// Verify the shape shows the updated font size value (18)
// This proves the remapping worked and the value update propagated through the reference
const fontSizeInput = workspacePage.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
await expect(fontSizeInput).toBeVisible();
await expect(fontSizeInput).toHaveValue("18");
});
});
test.describe("Border Radius Token Remapping", () => {
test("User renames border radius token with alias references", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("base-radius");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("card-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{base-radius}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename base token
const baseToken = tokensSidebar.getByRole("button", {
name: "base-radius",
});
await baseToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("primary-radius");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Check for remapping modal
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "primary-radius" }),
).toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "card-radius" }),
).toBeVisible();
});
test("User renames and updates border radius token - referenced token updates", async ({
page,
}) => {
const {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
// Create base border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
let nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-sm");
let valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("4");
let submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Create derived border radius token
await tokensTabPanel
.getByRole("button", { name: "Add Token: Border Radius" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("button-radius");
const valueField2 = tokensUpdateCreateModal.getByLabel("Value");
await valueField2.fill("{radius-sm}");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
// Rename and update value of base token
const radiusToken = tokensSidebar.getByRole("button", {
name: "radius-sm",
});
await radiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("radius-base");
// Update the value
valueField = tokensUpdateCreateModal.getByLabel("Value");
await valueField.fill("8");
submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await submitButton.click();
// Confirm remapping
const remappingModal = page.getByTestId("token-remapping-modal");
await expect(remappingModal).toBeVisible({ timeout: 5000 });
const confirmButton = remappingModal.getByRole("button", {
name: /remap/i,
});
await confirmButton.click();
// Verify base token was renamed
await expect(
tokensSidebar.getByRole("button", { name: "radius-base" }),
).toBeVisible();
// Verify referenced token still exists
await expect(
tokensSidebar.getByRole("button", { name: "button-radius" }),
).toBeVisible();
// Verify the referenced token now points to the renamed token
// by opening it and checking the reference
const buttonRadiusToken = tokensSidebar.getByRole("button", {
name: "button-radius",
});
await buttonRadiusToken.click({ button: "right" });
await tokenContextMenuForToken.getByText("Edit token").click();
await expect(tokensUpdateCreateModal).toBeVisible();
const currentValue = tokensUpdateCreateModal.getByLabel("Value");
await expect(currentValue).toHaveValue("{radius-base}");
});
});
});

View File

@@ -332,33 +332,24 @@ test("Copy/paste properties", async ({ page, context }) => {
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page
.getByTestId("layer-item")
.getByText("Rectangle")
.first()
.click({ button: "right" });
await page.getByText("Rectangle").first().click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page.getByText("Board").nth(2).click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page
.getByTestId("layer-item")
.getByText("Board")
.locator("div")
.filter({ hasText: "Path" })
.nth(1)
.click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page
.getByTestId("layer-item")
.getByText("Path")
.click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
await page
.getByTestId("layer-item")
.getByText("Ellipse")
.click({ button: "right" });
await page.getByText("Ellipse").click({ button: "right" });
await page.getByText("Copy/Paste as").hover();
await page.getByText("Paste properties").click();
});

View File

@@ -667,9 +667,6 @@
}
// UI ELEMENTS
// FIXME: This is used multiple times accross the app. We should design this in
// the DS and create a proper component for it.
.asset-element {
@include bodySmallTypography;
display: flex;

View File

@@ -245,6 +245,13 @@
--assets-component-second-border-selected: var(--color-background-primary);
--assets-component-hightlight: var(--color-accent-secondary);
--radio-btns-background-color: var(--color-background-tertiary);
--radio-btn-background-color-selected: var(--color-background-quaternary);
--radio-btn-foreground-color: var(--color-foreground-secondary);
--radio-btn-foreground-color-selected: var(--color-accent-primary);
--radio-btn-border-color: var(--color-background-tertiary);
--radio-btn-border-color-selected: var(--color-background-quaternary);
--library-name-foreground-color: var(--color-foreground-primary);
--library-content-foreground-color: var(--color-foreground-secondary);
@@ -417,6 +424,13 @@
--tab-border-color: var(--color-background-tertiary);
--tab-border-color-selected: var(--color-background-secondary);
--radio-btns-background-color: var(--color-background-tertiary);
--radio-btn-background-color-selected: var(--color-background-primary);
--radio-btn-foreground-color: var(--color-foreground-secondary);
--radio-btn-foreground-color-selected: var(--color-accent-primary);
--radio-btn-border-color: var(--color-background-tertiary);
--radio-btn-border-color-selected: var(--color-background-secondary);
--button-icon-background-color-selected: var(--color-background-primary);
--button-icon-border-color-selected: var(--color-background-secondary);

View File

@@ -8,7 +8,7 @@
(:require
["@tokens-studio/sd-transforms" :as sd-transforms]
["style-dictionary$default" :as sd]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.time :as ct]
@@ -83,7 +83,7 @@
[value]
(let [number? (or (number? value)
(numeric-string? value))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
@@ -109,7 +109,7 @@
"Parses `value` of a number `sd-token` into a map like `{:value 1 :unit \"px\"}`.
If the `value` is not parseable and/or has missing references returns a map with `:errors`."
[value]
(let [parsed-value (cfo/parse-token-value value)
(let [parsed-value (cft/parse-token-value value)
out-of-bounds (or (>= (:value parsed-value) sm/max-safe-int)
(<= (:value parsed-value) sm/min-safe-int))]
(if (and parsed-value (not out-of-bounds))
@@ -127,7 +127,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-scope (not (<= 0 (:value parsed-value) 1))
references (seq (cto/find-token-value-references value))]
(cond (and parsed-value (not out-of-scope))
@@ -151,7 +151,7 @@
If the `value` is parseable but is out of range returns a map with `warnings`."
[value]
(let [missing-references? (seq (cto/find-token-value-references value))
parsed-value (cfo/parse-token-value value)
parsed-value (cft/parse-token-value value)
out-of-scope (< (:value parsed-value) 0)
references (seq (cto/find-token-value-references value))]
(cond
@@ -236,7 +236,7 @@
Uses `font-size-value` to calculate the relative line-height value.
Returns an error for an invalid font-size value."
[line-height-value font-size-value font-size-errors]
(let [missing-references (seq (cto/find-token-value-references line-height-value))
(let [missing-references (seq (some cto/find-token-value-references line-height-value))
error
(cond
missing-references
@@ -250,7 +250,7 @@
:font-size-value font-size-value})]
(or error
(try
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
(case unit
"%" (/ value 100)
"px" (/ value font-size-value)

View File

@@ -1122,7 +1122,7 @@
ref-id (:stroke-color-ref-id stroke)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
@@ -1167,7 +1167,7 @@
ref-file (get color :ref-file)
ref-id (get color :ref-id)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
@@ -1180,20 +1180,19 @@
:index (:index shadow)}))
(defn- text->color-att
[fill file-id libraries & {:keys [has-token-applied token-name]}]
[fill file-id libraries]
(let [ref-file (:fill-color-ref-file fill)
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)
base-attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))
attrs (cond-> base-attrs
has-token-applied (assoc :has-token-applied true)
token-name (assoc :token-name token-name))]
attrs (cond-> (types.fills/fill->color fill)
(not (or shared? (= ref-file file-id)))
(dissoc :ref-file :ref-id))]
{:attrs attrs
:prop :content
:shape-id (:shape-id fill)
@@ -1201,18 +1200,13 @@
(defn- extract-text-colors
[text file-id libraries]
(let [applied-fill-token (get-in text [:applied-tokens :fill])
treat-node
(let [treat-node
(fn [node shape-id]
(map-indexed (fn [idx fill]
(let [args (cond-> []
(and (= idx 0) applied-fill-token)
(conj :has-token-applied true :token-name applied-fill-token))]
(apply text->color-att (assoc fill :shape-id shape-id :index idx) file-id libraries args)))
node))]
(map-indexed #(assoc %2 :shape-id shape-id :index %1) node))]
(->> (txt/node-seq txt/is-text-node? (:content text))
(map :fills)
(mapcat #(treat-node % (:id text))))))
(mapcat #(treat-node % (:id text)))
(map #(text->color-att % file-id libraries)))))
(defn- fill->color-att
"Given a fill map enriched with :shape-id, :index, and optionally
@@ -1238,7 +1232,7 @@
ref-id (:fill-color-ref-id fill)
colors (-> libraries
(get ref-file)
(get ref-id)
(get :data)
(ctl/get-colors))
shared? (contains? colors ref-id)

View File

@@ -7,7 +7,7 @@
(ns app.main.data.workspace.tokens.application
(:require
[app.common.data :as d]
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.common.types.component :as ctk]
[app.common.types.shape.layout :as ctsl]
[app.common.types.shape.radius :as ctsr]
@@ -525,8 +525,8 @@
shape-ids (d/nilv (keys shapes) [])
any-variant? (->> shapes vals (some ctk/is-variant?) boolean)
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
tokenized-attributes (cfo/attributes-map attributes token)
resolved-value (get-in resolved-tokens [(cft/token-identifier token) :resolved-value])
tokenized-attributes (cft/attributes-map attributes token)
type (:type token)]
(rx/concat
(rx/of
@@ -585,7 +585,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
(dwsh/update-shapes
shape-ids
(fn [shape]
@@ -613,7 +613,7 @@
(get token-properties (:type token))
unapply-tokens?
(cfo/shapes-token-applied? token shapes (or attrs all-attributes attributes))
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]

View File

@@ -6,7 +6,7 @@
(ns app.main.data.workspace.tokens.color
(:require
[app.common.files.tokens :as cfo]
[app.common.files.tokens :as cft]
[app.main.data.tinycolor :as tinycolor]))
(defn color-bullet-color [token-color-value]
@@ -17,5 +17,5 @@
(tinycolor/->hex-string tc))))
(defn resolved-token-bullet-color [{:keys [resolved-value] :as token}]
(when (and resolved-value (cfo/color-token? token))
(when (and resolved-value (cft/color-token? token))
(color-bullet-color resolved-value)))

View File

@@ -74,7 +74,7 @@
(when unknown-tokens
(st/emit! (show-unknown-types-warning unknown-tokens)))
(try
(->> (ctob/get-all-tokens-map tokens-lib)
(->> (ctob/get-all-tokens tokens-lib)
(sd/resolve-tokens-with-verbose-errors)
(rx/map (fn [_]
tokens-lib))

View File

@@ -11,7 +11,6 @@
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.logic.tokens :as clt]
[app.common.path-names :as cpn]
[app.common.types.shape :as cts]
[app.common.types.tokens-lib :as ctob]
[app.common.uuid :as uuid]
@@ -23,7 +22,6 @@
[app.main.data.workspace.tokens.propagation :as dwtp]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
(declare set-selected-token-set-id)
@@ -149,30 +147,27 @@
(defn create-token-set
[token-set]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema?
(ptk/reify ::create-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
ptk/UpdateEvent
(update [_ state]
;; Clear possible local state
(update state :workspace-tokens dissoc :token-set-new-path))
(defn rename-token-set
[token-set new-name]
(assert (ctob/token-set? token-set) "a token set is required") ;; TODO should check token-set-schema after renaming?
(assert (string? new-name) "a new name is required") ;; TODO should assert normalized-set-name?
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) new-name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))
(let [data (dsh/lookup-file-data state)
tokens-lib (get data :tokens-lib)
token-set (ctob/rename token-set (ctob/normalize-set-name (ctob/get-name token-set)))]
(if (and tokens-lib (ctob/get-set-by-name tokens-lib (ctob/get-name token-set)))
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/set-token-set (ctob/get-id token-set) token-set))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(defn rename-token-set-group
[set-group-path set-group-fname]
@@ -184,6 +179,26 @@
(rx/of
(dch/commit-changes changes))))))
(defn update-token-set
[token-set name]
(ptk/reify ::update-token-set
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
name (ctob/normalize-set-name name (ctob/get-name token-set))
tokens-lib (get data :tokens-lib)]
(if (ctob/get-set-by-name tokens-lib name)
(rx/of (ntf/show {:content (tr "errors.token-set-already-exists")
:type :toast
:level :error
:timeout 9000}))
(let [changes (-> (pcb/empty-changes it)
(pcb/with-library-data data)
(pcb/rename-token-set (ctob/get-id token-set) name))]
(rx/of (set-selected-token-set-id (ctob/get-id token-set))
(dch/commit-changes changes))))))))
(defn duplicate-token-set
[id]
(ptk/reify ::duplicate-token-set
@@ -433,7 +448,7 @@
(ctob/get-id token-set)
token-id)]
(let [tokens (vals (ctob/get-tokens tokens-lib (ctob/get-id token-set)))
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
unames (map :name tokens)
suffix (tr "workspace.tokens.duplicate-suffix")
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
new-token (-> token
@@ -445,35 +460,12 @@
;; TOKEN UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn clean-tokens-paths
[]
(ptk/reify ::clean-tokens-paths
(defn set-token-type-section-open
[token-type open?]
(ptk/reify ::set-token-type-section-open
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-tokens :unfolded-token-paths] []))))
(defn toggle-token-path
[path]
(ptk/reify ::toggle-token-path
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-tokens :unfolded-token-paths]
(fn [paths]
(let [paths (or paths [])]
(if (some #(= % path) paths)
(vec (remove #(or (= % path)
(str/starts-with? % (str path ".")))
paths))
(let [split-path (cpn/split-path path :separator ".")
partial-paths (reduce
(fn [acc segment]
(let [new-acc (if (empty? acc)
segment
(str (last acc) "." segment))]
(conj acc new-acc)))
[]
split-path)]
(into paths partial-paths)))))))))
(update-in state [:workspace-tokens :open-status-by-type] assoc token-type open?))))
(defn assign-token-context-menu
[{:keys [position] :as params}]

View File

@@ -22,9 +22,6 @@
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(l/set-level! :warn)
;; Helpers ---------------------------------------------------------------------
;; TODO: see if this can be replaced by more standard functions

View File

@@ -1,177 +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.data.workspace.tokens.remapping
"Core logic for token remapping functionality"
(:require
[app.common.files.changes-builder :as pcb]
[app.common.files.tokens :as cft]
[app.common.logging :as log]
[app.common.types.container :refer [shapes-seq]]
[app.common.types.file :refer [object-containers-seq]]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.changes :as dch]
[app.main.data.helpers :as dh]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
;; Token Reference Scanning
;; ========================
(defn scan-shape-applied-tokens
"Scan a shape for applied token references to a specific token name"
[shape token-name container]
(when-let [applied-tokens (:applied-tokens shape)]
(for [[attribute applied-token-name] applied-tokens
:when (= applied-token-name token-name)]
{:type :applied-token
:shape-id (:id shape)
:attribute attribute
:token-name applied-token-name
:container container})))
(defn scan-token-value-references
"Scan a token value for references to a specific token name (alias), supporting complex token values."
[token token-name]
(letfn [(find-all-token-value-references [token-value]
(cond
(string? token-value)
(filter #(= % token-name) (cto/find-token-value-references token-value))
(map? token-value)
(mapcat find-all-token-value-references (vals token-value))
(sequential? token-value)
(mapcat find-all-token-value-references token-value)
:else
[]))]
(when-let [value (:value token)]
(for [referenced-token-name (find-all-token-value-references value)]
{:type :token-alias
:source-token-id (:id token)
:referenced-token-name referenced-token-name}))))
(defn scan-workspace-token-references
"Scan entire workspace for all token references to a specific token"
[file-data old-token-name]
(let [tokens-lib (:tokens-lib file-data)
containers (object-containers-seq file-data)
;; Scan all shapes for applied token references to the specific token
matching-applied (mapcat (fn [container]
(let [shapes (shapes-seq container)]
(mapcat #(scan-shape-applied-tokens % old-token-name container) shapes)))
containers)
;; Scan tokens library for alias references to the specific token
matching-aliases (if tokens-lib
(let [all-tokens (ctob/get-all-tokens tokens-lib)]
(mapcat #(scan-token-value-references % old-token-name) all-tokens))
[])]
(log/info :hint "token-scan-details"
:token-name old-token-name
:containers-count (count containers)
:total-applied-refs (count matching-applied)
:matching-applied (count matching-applied)
:total-alias-refs (count matching-aliases)
:matching-aliases (count matching-aliases))
{:applied-tokens matching-applied
:token-aliases matching-aliases
:total-references (+ (count matching-applied) (count matching-aliases))}))
;; Token Remapping Core Logic
;; ==========================
(defn remap-tokens
"Main function to remap all token references when a token name changes"
[old-token-name new-token-name]
(ptk/reify ::remap-tokens
ptk/WatchEvent
(watch [_ state _]
(let [file-data (dh/lookup-file-data state)
scan-results (scan-workspace-token-references file-data old-token-name)
tokens-lib (:tokens-lib file-data)
sets (ctob/get-sets tokens-lib)
tokens-with-sets (mapcat (fn [set]
(map (fn [token]
{:token token :set set})
(vals (ctob/get-tokens tokens-lib (ctob/get-id set)))))
sets)
;; Group applied token references by container
refs-by-container (group-by :container (:applied-tokens scan-results))
;; Use apply-token logic to update shapes for both direct and alias references
shape-changes (reduce-kv
(fn [changes container refs]
(let [shape-ids (map :shape-id refs)
;; Find the correct token to apply (new or alias)
token (or (some #(when (= (:name (:token %)) new-token-name) %) tokens-with-sets)
(some #(when (= (:name (:token %)) old-token-name) %) tokens-with-sets))
attributes (set (map :attribute refs))]
(if token
(-> (pcb/with-container changes container)
(pcb/update-shapes shape-ids
(fn [shape]
(update shape :applied-tokens
#(merge % (cft/attributes-map attributes (:token token)))))))
changes)))
(-> (pcb/empty-changes)
(pcb/with-file-data file-data)
(pcb/with-library-data file-data))
refs-by-container)
;; Create changes for updating token alias references
token-changes (reduce
(fn [changes ref]
(let [source-token-id (:source-token-id ref)]
(when-let [{:keys [token set]} (some #(when (= (:id (:token %)) source-token-id) %) tokens-with-sets)]
(let [old-value (:value token)
new-value (cto/update-token-value-references old-value old-token-name new-token-name)]
(pcb/set-token changes (ctob/get-id set) (:id token)
(assoc token :value new-value))))))
shape-changes
(:token-aliases scan-results))]
(log/info :hint "token-remapping"
:old-name old-token-name
:new-name new-token-name
:references-count (:total-references scan-results))
(rx/of (dch/commit-changes token-changes))))))
(defn validate-token-remapping
"Validate that a token remapping operation is safe to perform"
[old-name new-name]
(cond
(str/blank? new-name)
{:valid? false
:error :invalid-name
:message "Token name cannot be empty"}
(= old-name new-name)
{:valid? false
:error :no-change
:message "New name is the same as current name"}
:else
{:valid? true}))
(defn count-token-references
"Count the number of references to a token in the workspace"
[file-data token-name]
(let [scan-results (scan-workspace-token-references file-data token-name)]
(log/info :hint "token-reference-scan"
:token-name token-name
:applied-refs (count (:applied-tokens scan-results))
:alias-refs (count (:token-aliases scan-results))
:total (:total-references scan-results))
(:total-references scan-results)))

View File

@@ -4,29 +4,22 @@
//
// Copyright (c) KALEIDOS INC
// FIXME: we need this import for .asset-element
@use "refactor/basic-rules.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/spacing.scss" as *;
@use "refactor/common-refactor.scss" as deprecated;
.editable-select {
@extend .asset-element;
margin: 0;
padding: 0;
border: $b-1 solid var(--input-border-color);
border: deprecated.$s-1 solid var(--input-border-color);
position: relative;
display: flex;
height: $sz-32;
height: deprecated.$s-32;
width: 100%;
padding: var(--sp-s);
border-radius: $br-8;
padding: deprecated.$s-8;
border-radius: deprecated.$br-8;
cursor: pointer;
.dropdown-button {
display: flex;
place-content: center;
@include deprecated.flexCenter;
svg {
@extend .button-icon-small;
transform: rotate(90deg);
@@ -36,11 +29,10 @@
.custom-select-dropdown {
@extend .dropdown-wrapper;
width: fit-content;
max-height: px2rem(320); // TODO: when this gets addressed in the DS, use a token
max-height: deprecated.$s-320;
.separator {
margin: 0;
height: $sz-12;
height: deprecated.$s-12;
}
.dropdown-element {
@extend .dropdown-element-base;
@@ -51,8 +43,7 @@
}
.check-icon {
display: flex;
place-content: center;
@include deprecated.flexCenter;
svg {
@extend .button-icon-small;
visibility: hidden;

View File

@@ -10,9 +10,9 @@
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(mf/defc file-uploader*
(mf/defc file-uploader
{::mf/forward-ref true}
[{:keys [accept multi label-text label-class input-id on-selected data-testid]} input-ref]
[{:keys [accept multi label-text label-class input-id on-selected data-testid] :as props} input-ref]
(let [opt-pick-one #(if multi % (first %))
on-files-selected

View File

@@ -315,8 +315,7 @@
gap: deprecated.$s-4;
max-height: deprecated.$s-136;
padding: deprecated.$s-4 0;
overflow-y: auto;
overflow-y: scroll;
.selected-item {
.around {
@include deprecated.flexRow;

View File

@@ -0,0 +1,107 @@
;; 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.components.radio-buttons
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.formats :as fmt]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(def context
(mf/create-context nil))
(mf/defc radio-button
{::mf/props :obj}
[{:keys [icon id value disabled title icon-class type]}]
(let [context (mf/use-ctx context)
allow-empty (unchecked-get context "allow-empty")
type (if ^boolean type
type
(if ^boolean allow-empty
"checkbox"
"radio"))
on-change (unchecked-get context "on-change")
selected (unchecked-get context "selected")
name (unchecked-get context "name")
encode-fn (unchecked-get context "encode-fn")
checked? (= selected value)
value (encode-fn value)]
[:label {:html-for id
:data-testid id
:title title
:class (stl/css-case
:radio-icon true
:checked checked?
:disabled disabled)}
(if (some? icon)
[:> icon* {:icon-id icon :class icon-class :aria-hidden true}]
[:span {:class (stl/css :title-name)} value])
[:input {:id id
:on-change on-change
:type type
:name name
:disabled disabled
:value value
:default-checked checked?}]]))
(mf/defc radio-buttons
{::mf/props :obj}
[{:keys [name children on-change selected class wide encode-fn decode-fn allow-empty] :as props}]
(let [encode-fn (d/nilv encode-fn identity)
decode-fn (d/nilv decode-fn identity)
nitems (if (array? children)
(count (keep identity children))
1)
;; FIXME: we should handle this with CSS
width (mf/with-memo [nitems]
(if (= wide true)
"unset"
(fmt/format-pixels
(+ (* 4 (- nitems 1))
(* 32 nitems)))))
on-change'
(mf/use-fn
(mf/deps selected on-change)
(fn [event]
(let [input (dom/get-target event)
value (dom/get-target-val event)
;; Only allow null values when the "allow-empty" prop is true
value (when (or (not allow-empty)
(not= value selected)) value)]
(when (fn? on-change)
(on-change (decode-fn value) event))
(dom/blur! input))))
context-value
(mf/spread-object props
;; We pass a special metadata for disable
;; key casing transformation in this
;; concrete case, because this component
;; uses legacy mode and props are in
;; kebab-case style
^{::mf/transform false}
{:on-change on-change'
:encode-fn encode-fn
:decode-fn decode-fn})]
[:& (mf/provider context) {:value context-value}
[:div {:class (dm/str class " " (stl/css :radio-btn-wrapper))
:style {:width width}
:key (dm/str name "-" selected)}
children]]))

View File

@@ -0,0 +1,79 @@
// 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
@use "refactor/common-refactor.scss" as deprecated;
.radio-btn-wrapper {
@include deprecated.flexCenter;
border-radius: deprecated.$br-8;
height: deprecated.$s-32;
background-color: var(--input-background-color);
gap: deprecated.$s-4;
}
.radio-icon {
--radio-icon-border-color: var(--radio-btn-border-color);
@include deprecated.buttonStyle;
@include deprecated.flexCenter;
@include deprecated.focusRadio;
height: deprecated.$s-32;
flex-grow: 1;
border-radius: deprecated.$s-8;
box-sizing: border-box;
border: deprecated.$br-2 solid var(--radio-icon-border-color);
input {
display: none;
}
svg {
@extend .button-icon;
stroke: var(--radio-btn-foreground-color);
}
.title-name {
@include deprecated.uppercaseTitleTipography;
color: var(--radio-btn-foreground-color);
}
&:hover {
svg {
stroke: var(--radio-btn-foreground-color-selected);
}
}
}
.checked {
--radio-icon-border-color: var(--radio-btn-border-color-selected);
background-color: var(--radio-btn-background-color-selected);
svg {
stroke: var(--radio-btn-foreground-color-selected);
}
.title-name {
color: var(--radio-btn-foreground-color-selected);
}
}
.disabled {
cursor: default;
background-color: transparent;
border: deprecated.$s-2 solid transparent;
svg {
stroke: var(--button-foreground-color-disabled);
}
.title-name {
color: var(--button-foreground-color-disabled);
}
&:hover {
background-color: transparent;
border: deprecated.$s-2 solid transparent;
svg {
stroke: var(--button-foreground-color-disabled);
}
.title-name {
color: var(--button-foreground-color-disabled);
}
}
}

View File

@@ -32,27 +32,6 @@
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(defn- on-restore-project
[project]
(let [on-accept #(st/emit! (dd/restore-project-immediately project))]
(st/emit! (modal/show
{:type :confirm
:title (tr "dashboard.restore-project-confirmation.title")
:message (tr "dashboard.restore-project-confirmation.description" (:name project))
:accept-style :primary
:accept-label (tr "labels.continue")
:on-accept on-accept}))))
(defn- on-delete-project
[project]
(let [accept-fn #(st/emit! (dd/delete-project-immediately project))]
(st/emit! (modal/show
{:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-project-forever-confirmation.description" (:name project))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn}))))
(mf/defc header*
{::mf/props :obj
::mf/private true}
@@ -61,8 +40,7 @@
[:div#dashboard-deleted-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.projects-title")]]])
(mf/defc project-context-menu*
{::mf/private true}
(mf/defc deleted-project-menu*
[{:keys [project show on-close top left]}]
(let [top (d/nilv top 0)
left (d/nilv left 0)
@@ -70,13 +48,25 @@
on-restore-project
(mf/use-fn
(mf/deps project)
(partial on-restore-project project))
(fn []
(let [on-accept #(st/emit! (dd/restore-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.restore-project-confirmation.title")
:message (tr "dashboard.restore-project-confirmation.description" (:name project))
:accept-style :primary
:accept-label (tr "labels.continue")
:on-accept on-accept})))))
on-delete-project
(mf/use-fn
(mf/deps project)
(partial on-delete-project project))
(fn []
(let [accept-fn #(st/emit! (dd/delete-project-immediately project))]
(st/emit! (modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
:message (tr "dashboard.delete-project-forever-confirmation.description" (:name project))
:accept-label (tr "dashboard.delete-forever-confirmation.title")
:on-accept accept-fn})))))
options
(mf/with-memo [on-restore-project on-delete-project]
[{:name (tr "dashboard.restore-project-button")
@@ -161,7 +151,7 @@
menu-icon]]
(when (:menu-open @local)
[:> project-context-menu*
[:> deleted-project-menu*
{:project project
:show (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
@@ -184,8 +174,8 @@
:limit limit
:selected-files selected-files}])]]))
(mf/defc menu*
{::mf/private true}
[{:keys [team-id section]}]
(let [on-recent-click
(mf/use-fn
@@ -232,8 +222,7 @@
(some #(= (:id project) (:project-id %))
(vals deleted-map)))))
(sort-by :modified-at)
(reverse)
(not-empty)))
(reverse)))
team-id
(get team :id)
@@ -284,44 +273,37 @@
[:*
[:> header* {:team team}]
[:section {:class (stl/css :dashboard-container :no-bg)
:data-testid "deleted-page-section"}
[:section {:class (stl/css :dashboard-container :no-bg)}
[:*
[:div {:class (stl/css :no-bg)}
[:> menu* {:team-id team-id :section :dashboard-deleted}]
(if (seq projects)
[:*
[:div {:class (stl/css :deleted-info-content)}
[:p {:class (stl/css :deleted-info)}
(tr "dashboard.trash-info-text-part1")
[:span {:class (stl/css :info-text-highlight)}
(tr "dashboard.trash-info-text-part2" deletion-days)]
(tr "dashboard.trash-info-text-part3")
[:br]
(tr "dashboard.trash-info-text-part4")]
[:div {:class (stl/css :deleted-options)}
[:> button* {:variant "ghost"
:type "button"
:on-click on-restore-all}
(tr "dashboard.restore-all-deleted-button")]
[:> button* {:variant "destructive"
:type "button"
:icon "delete"
:on-click on-delete-all}
(tr "dashboard.clear-trash-button")]]]
[:div {:class (stl/css :deleted-info-content)}
[:p {:class (stl/css :deleted-info)}
(tr "dashboard.trash-info-text-part1")
[:span {:class (stl/css :info-text-highlight)}
(tr "dashboard.trash-info-text-part2" deletion-days)]
(tr "dashboard.trash-info-text-part3")
[:br]
(tr "dashboard.trash-info-text-part4")]
[:div {:class (stl/css :deleted-options)}
[:> button* {:variant "ghost"
:type "button"
:on-click on-restore-all}
(tr "dashboard.restore-all-deleted-button")]
[:> button* {:variant "destructive"
:type "button"
:icon "delete"
:on-click on-delete-all}
(tr "dashboard.clear-trash-button")]]]
(for [{:keys [id] :as project} projects]
(let [files (when deleted-map
(->> (vals deleted-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:> deleted-project-item* {:project project
:files files
:key id}]))]
;; when no deleted projects
[:div {:class (stl/css :deleted-info-content)}
[:p {:class (stl/css :deleted-info)}
(tr "dashboard.deleted.empty-state-description")]])]]]]))
(when (seq projects)
(for [{:keys [id] :as project} projects]
(let [files (when deleted-map
(->> (vals deleted-map)
(filterv #(= id (:project-id %)))
(sort-by :modified-at #(compare %2 %1))))]
[:> deleted-project-item* {:project project
:files files
:key id}])))]]]]))

View File

@@ -51,6 +51,10 @@
padding: var(--sp-xxl) var(--sp-xxl) var(--sp-s) var(--sp-xxl);
position: sticky;
top: 0;
// We need to use the the deprecated z-index so it won't clash with the dashboard
// onboarding modals
z-index: deprecated.$z-index-3;
}
.nav-inside {

View File

@@ -6,7 +6,6 @@
(ns app.main.ui.dashboard.file-menu
(:require
[app.common.data :as d]
[app.main.data.common :as dcm]
[app.main.data.dashboard :as dd]
[app.main.data.event :as-alias ev]
@@ -90,12 +89,12 @@
on-duplicate
(fn [_]
(apply st/emit! (map dd/duplicate-file files))
(st/emit! (ntf/success (tr "dashboard.success-duplicate-file" (i18n/c file-count)))))
(st/emit! (ntf/success (tr "dashboard.success-duplicate-file" (i18n/c (count files))))))
on-delete-accept
(fn [_]
(apply st/emit! (map dd/delete-file files))
(st/emit! (ntf/success (tr "dashboard.success-delete-file" (i18n/c file-count)))
(st/emit! (ntf/success (tr "dashboard.success-delete-file" (i18n/c (count files))))
(dd/clear-selected-files)))
on-delete
@@ -194,7 +193,7 @@
(fn [_]
(st/emit! (dd/restore-files-immediately
(with-meta {:team-id (:id current-team)
:ids (into #{} d/xf:map-id files)}
:ids #{(:id file)}}
{:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
(dd/fetch-projects (:id current-team))
(dd/fetch-deleted-files (:id current-team)))
@@ -202,7 +201,6 @@
on-restore-immediately
(fn []
(prn files)
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard-restore-file-confirmation.title")
@@ -215,7 +213,7 @@
(fn []
(let [accept-fn #(st/emit! (dd/delete-files-immediately
{:team-id (:id current-team)
:ids (into #{} d/xf:map-id files)}))]
:ids #{(:id file)}}))]
(st/emit!
(modal/show {:type :confirm
:title (tr "dashboard.delete-forever-confirmation.title")
@@ -262,12 +260,14 @@
options
(if can-restore
[{:name (tr "dashboard.file-menu.restore-files-option" (i18n/c file-count))
:id "restore-file"
:handler on-restore-immediately}
{:name (tr "dashboard.file-menu.delete-files-permanently-option" (i18n/c file-count))
:id "delete-file"
:handler on-delete-immediately}]
[(when can-restore
{:name (tr "dashboard.restore-file-button")
:id "restore-file"
:handler on-restore-immediately})
(when can-restore
{:name (tr "dashboard.delete-file-button")
:id "delete-file"
:handler on-delete-immediately})]
(if multi?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)

View File

@@ -17,7 +17,7 @@
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.context-notification :refer [context-notification]]
@@ -184,11 +184,11 @@
:on-click on-click
:tab-index "0"}
[:span (tr "labels.add-custom-font")]
[:> file-uploader* {:input-id "font-upload"
:accept accept-font-types
:multi true
:ref input-ref
:on-selected on-selected}]]
[:& file-uploader {:input-id "font-upload"
:accept accept-font-types
:multi true
:ref input-ref
:on-selected on-selected}]]
(when-let [url cf/terms-of-service-uri]
[:& context-notification {:content (tr "dashboard.fonts.hero-text2" url)

View File

@@ -16,7 +16,7 @@
[app.main.data.notifications :as ntf]
[app.main.errors :as errors]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.context-notification :refer [context-notification]]
@@ -58,10 +58,10 @@
[{:keys [project-id on-finish-import]} external-ref]
(let [on-file-selected (use-import-file project-id on-finish-import)]
[:form.import-file {:aria-hidden "true"}
[:> file-uploader* {:accept ".penpot,.zip"
:multi true
:ref external-ref
:on-selected on-file-selected}]]))
[:& file-uploader {:accept ".penpot,.zip"
:multi true
:ref external-ref
:on-selected on-file-selected}]]))
(defn- update-entry-name
[entries file-id new-name]

View File

@@ -4,36 +4,35 @@
//
// Copyright (c) KALEIDOS INC
@use "common/refactor/common-refactor.scss" as deprecated;
@use "common/refactor/common-dashboard";
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/z-index.scss" as *;
@use "ds/mixins.scss" as *;
@use "ds/_utils.scss" as *;
@use "../ds/typography.scss" as t;
@use "../ds/_borders.scss" as *;
@use "../ds/spacing.scss" as *;
@use "../ds/_sizes.scss" as *;
@use "../ds/z-index.scss" as *;
.dashboard-container {
flex: 1 0 0;
inline-size: 100%;
width: 100%;
margin-inline-end: var(--sp-l);
border-block-start: $b-1 solid var(--panel-border-color);
border-top: $b-1 solid var(--panel-border-color);
overflow-y: auto;
padding-block-end: var(--sp-xxxl);
padding-bottom: var(--sp-xxxl);
}
.dashboard-projects {
user-select: none;
block-size: calc(100vh - px2rem(64));
height: calc(100vh - deprecated.$s-64);
}
.with-team-hero {
block-size: calc(100vh - px2rem(280));
height: calc(100vh - deprecated.$s-280);
}
.dashboard-shared {
inline-size: calc(100vw - px2rem(320));
margin-inline-end: px2rem(52);
width: calc(100vw - deprecated.$s-320);
margin-inline-end: deprecated.$s-52;
}
.search {
@@ -67,8 +66,8 @@
align-items: center;
justify-content: space-between;
gap: var(--sp-s);
inline-size: 99%;
max-block-size: $sz-40;
width: 99%;
max-height: $sz-40;
padding: var(--sp-s) var(--sp-s) var(--sp-s) var(--sp-l);
margin-block-start: var(--sp-l);
border-radius: $br-4;
@@ -78,19 +77,19 @@
display: flex;
align-items: center;
justify-content: flex-start;
inline-size: 100%;
min-block-size: $sz-32;
width: 100%;
min-height: var(--sp-xxxl);
margin-inline-start: var(--sp-s);
}
.project-name {
@include textEllipsis;
@include t.use-typography("body-large");
width: fit-content;
margin-inline-end: var(--sp-m);
line-height: 0.8;
color: var(--title-foreground-color-hover);
cursor: pointer;
block-size: $sz-16;
line-height: 0.8;
margin-inline-end: var(--sp-m);
height: var(--sp-l);
}
.info-wrapper {
@@ -117,8 +116,8 @@
.add-file-btn,
.options-btn {
@extend .button-tertiary;
block-size: $sz-32;
inline-size: $sz-32;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
margin: 0 var(--sp-s);
padding: var(--sp-s);
}
@@ -130,7 +129,7 @@
}
.grid-container {
inline-size: 100%;
width: 100%;
padding: 0 var(--sp-xs);
}
@@ -140,13 +139,11 @@
.show-more {
--show-more-color: var(--button-secondary-foreground-color-rest);
@include deprecated.buttonStyle;
@include t.use-typography("body-medium");
border: none;
background: none;
cursor: pointer;
position: absolute;
inset-block-start: var(--sp-s);
inset-inline-end: px2rem(52);
top: var(--sp-s);
right: deprecated.$s-52;
display: flex;
align-items: center;
justify-content: space-between;
@@ -159,8 +156,8 @@
}
.show-more-icon {
block-size: $sz-16;
inline-size: $sz-16;
height: var(--sp-l);
width: var(--sp-l);
fill: none;
stroke: var(--show-more-color);
}
@@ -168,7 +165,7 @@
// Team hero
.team-hero {
background-color: var(--color-background-tertiary);
border-radius: $br-8;
border-radius: deprecated.$br-8;
border: none;
display: flex;
margin: var(--sp-l);
@@ -177,11 +174,12 @@
img {
border-radius: $br-4;
inline-size: auto;
height: var(--sp-xl) 0;
width: auto;
@media (max-width: 1200px) {
display: none;
inline-size: 0;
width: 0;
}
}
}
@@ -195,8 +193,9 @@
}
.title {
font-size: $sz-24;
color: var(--color-foreground-primary);
font-size: px2rem(24);
font-weight: deprecated.$fw400;
}
.info {
@@ -216,8 +215,8 @@
--close-icon-foreground-color: var(--icon-foreground);
position: absolute;
top: var(--sp-xl);
inset-inline-end: var(--sp-xxl);
inline-size: var(--sp-xxl);
right: var(--sp-xxl);
width: var(--sp-xxl);
background-color: transparent;
border: none;
cursor: pointer;
@@ -232,20 +231,20 @@
}
.invite {
block-size: $sz-32;
inline-size: px2rem(180);
height: var(--sp-xxxl);
width: deprecated.$s-180;
}
.img-wrapper {
display: flex;
align-items: center;
justify-content: center;
inline-size: var(--sp-xl) 0;
block-size: var(--sp-xl) 0;
width: var(--sp-xl) 0;
height: var(--sp-xl) 0;
overflow: hidden;
border-radius: $br-4;
border-radius: deprecated.$br-4;
@media (max-width: 1200px) {
display: none;
inline-size: 0;
width: 0;
}
}

View File

@@ -19,7 +19,7 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.main.ui.dashboard.change-owner]
[app.main.ui.dashboard.subscription :refer [members-cta*
@@ -1315,10 +1315,10 @@
[:img {:class (stl/css :team-image)
:src (cfg/resolve-team-photo-url team)}]
(when can-edit
[:> file-uploader* {:accept "image/jpeg,image/png"
:multi false
:ref finput
:on-selected on-file-selected}])]
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:ref finput
:on-selected on-file-selected}])]
[:div {:class (stl/css :block-label)}
(tr "dashboard.team-info")]
[:div {:class (stl/css :block-text)}

View File

@@ -11,10 +11,8 @@
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k]
@@ -99,11 +97,8 @@
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} title]
[:> icon-button* {:variant "ghost"
:class (stl/css :modal-close-btn)
:icon i/close
:aria-label (tr "labels.close")
:on-click cancel-fn}]]
[:button {:class (stl/css :modal-close-btn)
:on-click cancel-fn} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
(when (and (string? subtitle) (not= subtitle ""))
@@ -129,10 +124,14 @@
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
(when-not (= cancel-label :omit)
[:> button* {:variant "secondary"
:on-click cancel-fn}
cancel-label])
[:input {:class (stl/css :cancel-button)
:type "button"
:value cancel-label
:on-click cancel-fn}])
[:> button* {:variant (if (= accept-style :danger) "destructive" "primary")
:on-click accept-fn}
accept-label]]]]]))
[:input {:class (stl/css-case :accept-btn true
:danger (= accept-style :danger)
:primary (= accept-style :primary))
:type "button"
:value accept-label
:on-click accept-fn}]]]]]))

View File

@@ -33,9 +33,7 @@
}
.modal-close-btn {
position: absolute;
top: var(--sp-s);
right: var(--sp-s);
@extend .modal-close-btn-base;
}
.modal-content {
@@ -55,6 +53,17 @@
@extend .modal-action-btns;
}
.cancel-button {
@extend .modal-cancel-btn;
}
.accept-btn {
@extend .modal-accept-btn;
&.danger {
@extend .modal-danger-btn;
}
}
.modal-scd-msg {
margin-block: 0;
}

View File

@@ -12,7 +12,6 @@
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.ds.controls.switch :refer [switch*]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
@@ -64,7 +63,6 @@
:Select select*
:Switch switch*
:Checkbox checkbox*
:RadioButtons radio-buttons*
:Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*

View File

@@ -15,7 +15,6 @@
display: inline-flex;
align-items: center;
justify-content: center;
column-gap: var(--sp-xs);
}

View File

@@ -1,107 +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.ds.controls.radio-buttons
(:require-macros
[app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
[app.util.dom :as dom]
[rumext.v2 :as mf]
[rumext.v2.util :as mfu]))
(def ^:private schema:radio-button
[:map
[:id :string]
[:icon {:optional true}
[:and :string [:fn #(contains? icon-list %)]]]
[:label :string]
[:value [:or :keyword :string]]
[:disabled {:optional true} :boolean]])
(def ^:private schema:radio-buttons
[:map
[:class {:optional true} :string]
[:variant {:optional true}
[:maybe [:enum "primary" "secondary" "ghost" "destructive" "action"]]]
[:extended {:optional true} :boolean]
[:name {:optional true} :string]
[:selected {:optional true}
[:maybe [:or :keyword :string]]]
[:allow-empty {:optional true} :boolean]
[:options [:vector {:min 1} schema:radio-button]]
[:on-change {:optional true} fn?]])
(mf/defc radio-buttons*
{::mf/schema schema:radio-buttons}
[{:keys [class variant extended name selected allow-empty options on-change] :rest props}]
(let [options (if (array? options)
(mfu/bean options)
options)
type (if allow-empty "checkbox" "radio")
variant (d/nilv variant "secondary")
handle-click
(mf/use-fn
(fn [event]
(let [target (dom/get-target event)
label (dom/get-parent-with-data target "label")]
(dom/prevent-default event)
(dom/stop-propagation event)
(dom/click label))))
handle-change
(mf/use-fn
(mf/deps selected on-change)
(fn [event]
(let [input (dom/get-target event)
value (dom/get-target-val event)]
(when (fn? on-change)
(on-change value event))
(dom/blur! input))))
props
(mf/spread-props props {:key (dm/str name "-" selected)
:class [class (stl/css-case :wrapper true
:extended extended)]})]
[:> :div props
(for [[idx {:keys [id class value label icon disabled]}] (d/enumerate options)]
(let [checked? (= selected value)]
[:label {:key idx
:html-for id
:data-label true
:data-testid id
:class [class (stl/css-case :label true
:extended extended)]}
(if (some? icon)
[:> icon-button* {:variant variant
:on-click handle-click
:aria-pressed checked?
:aria-label label
:icon icon
:disabled disabled}]
[:> button* {:variant variant
:on-click handle-click
:aria-pressed checked?
:class (stl/css-case :button true
:extended extended)
:disabled disabled}
label])
[:input {:id id
:class (stl/css :input)
:on-change handle-change
:type type
:name name
:disabled disabled
:value value
:default-checked checked?}]]))]))

View File

@@ -1,97 +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 { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as RadioButtons from "./radio_buttons.stories";
<Meta title="Controls/Radio Buttons" />
# Radio Buttons
The `radio-buttons*` component allows users to switch between two or more options that are mutually exclusive.
## Variants
Radio buttons with text only. The label will be the text of the button.
<Canvas of={RadioButtons.Default} />
```clj
[:> radio-buttons* {:selected "left"
:on-change handle-change
:name "alignment"
:extended false
:allow-empty false
:options [{:id "align-left"
:label "Left"
:value "left"}
{:id "align-center"
:label "Center"
:value "center"}
{:id "align-right"
:label "Right"
:value "right"}]}]
```
Radio buttons with icons only. In this case, the label will act as the tooltip of each button.
<Canvas of={RadioButtons.WithIcons} />
```clj
(ns app.main.ui.foo
(:require
[app.main.ui.ds.foundations.assets.icon :as i]))
[:> radio-buttons* {:selected "left"
:on-change handle-change
:name "alignment"
:extended false
:allow-empty false
:options [{:id "align-left"
:icon i/text-align-left
:label "Left align"
:value "left"}
{:id "align-center"
:icon i/text-align-center
:label "Center align"
:value "center"}
{:id "align-right"
:icon i/text-align-right
:label "Right align"
:value "right"}]}]
```
## Anatomy
Under the hood, each option is represented by
- a button, which is the visible and clickable element. It may be either an icon button or a text button.
- a radio input, which is not visible but retains the current state of the option.
A radio group is defined by giving each of radio buttons in the group the same name. Once a radio group is established,
selecting any radio button in that group automatically deselects any currently-selected radio button in the same group.
The `selected` parameter should be set to the value of the option that is to be active. Otherwise, no option will be selected.
If the parameter `allow-empty` is enabled, then the component will work with checkboxes instead of radio buttons,
and therefore the selected option can be deselected. However, it will still only be possible to select one option.
The `extended` parameter allows the component to use all the available space from the parent and distribute it equally
among all elements.
Any option can be individually disabled using the `disabled` parameter.
## Usage Guidelines
### When to Use
- For multiple choice settings that take effect immediately.
- In preference panels and configuration screens.
### When Not to Use
- For boolean settings (use switch or checkbox instead).
- For actions that require confirmation (use buttons instead).
- For temporary states that need explicit "Apply" action.

View File

@@ -1,40 +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
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
.wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
width: fit-content;
border-radius: $br-8;
background-color: var(--color-background-tertiary);
gap: var(--sp-xs);
&.extended {
width: 100%;
display: flex;
}
}
.label {
&.extended {
display: flex;
flex: 1 1 0;
}
}
.button {
&.extended {
flex-grow: 1;
}
}
.input {
display: none;
}

View File

@@ -1,72 +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 * as React from "react";
import Components from "@target/components";
const { RadioButtons } = Components;
const options = [
{id: "left", label: "Left", value: "left" },
{id: "center", label: "Center", value: "center" },
{id: "right", label: "Right", value: "right" },
];
const optionsIcon = [
{id: "left", label: "Left align", value: "left", icon: "text-align-left" },
{id: "center", label: "Center align", value: "center", icon: "text-align-center" },
{id: "right", label: "Right align", value: "right", icon: "text-align-right" },
];
export default {
title: "Controls/Radio Buttons",
component: RadioButtons,
argTypes: {
name: {
control: { type: "text" },
description: "Whether the checkbox is checked",
},
selected: {
control: { type: "select" },
options: ["", "left", "center", "right"],
description: "Whether the checkbox is checked",
},
extended: {
control: { type: "boolean" },
description: "Whether the checkbox is checked",
},
allowEmpty: {
control: { type: "boolean" },
description: "Whether the checkbox is checked",
},
disabled: {
control: { type: "boolean" },
description: "Whether the checkbox is disabled",
},
},
args: {
name: "alignment",
selected: "left",
extended: false,
allowEmpty: false,
options: options,
disabled: false,
},
parameters: {
controls: {
exclude: ["options", "on-change"],
},
},
render: ({ ...args }) => <RadioButtons {...args} />,
};
export const Default = {};
export const WithIcons = {
args: {
options: optionsIcon,
},
};

View File

@@ -208,7 +208,7 @@
;; FIXME: deprecated, should be refactored in two components and use
;; the generic progress reporter
(mf/defc progress-widget*
(mf/defc progress-widget
{::mf/wrap [mf/memo]}
[]
(let [state (mf/deref refs/export)

View File

@@ -121,7 +121,7 @@
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)}
(tr "files-download-modal.title")]
(tr "dashboard.export.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click on-cancel} deprecated-icon/close]]
@@ -129,8 +129,8 @@
(= status :prepare)
[:*
[:div {:class (stl/css :modal-content)}
[:p {:class (stl/css :modal-msg)} (tr "files-download-modal.description-1")]
[:p {:class (stl/css :modal-scd-msg)} (tr "files-download-modal.description-2")]
[:p {:class (stl/css :modal-msg)} (tr "dashboard.export.explain")]
[:p {:class (stl/css :modal-scd-msg)} (tr "dashboard.export.detail")]
(for [type fexp/valid-types]
[:div {:class (stl/css :export-option true)
@@ -138,20 +138,20 @@
[:label {:for (str "export-" type)
:class (stl/css-case :global/checked (= selected type))}
;; Execution time translation strings:
;; (tr "files-download-modal.options.all.message")
;; (tr "files-download-modal.options.all.title")
;; (tr "files-download-modal.options.detach.message")
;; (tr "files-download-modal.options.detach.title")
;; (tr "files-download-modal.options.merge.message")
;; (tr "files-download-modal.options.merge.title")
;; (tr "dashboard.export.options.all.message")
;; (tr "dashboard.export.options.all.title")
;; (tr "dashboard.export.options.detach.message")
;; (tr "dashboard.export.options.detach.title")
;; (tr "dashboard.export.options.merge.message")
;; (tr "dashboard.export.options.merge.title")
[:span {:class (stl/css-case :global/checked (= selected type))}
(when (= selected type)
deprecated-icon/status-tick)]
[:div {:class (stl/css :option-content)}
[:h3 {:class (stl/css :modal-subtitle)}
(tr (dm/str "files-download-modal.options." (d/name type) ".title"))]
(tr (dm/str "dashboard.export.options." (d/name type) ".title"))]
[:p {:class (stl/css :modal-msg)}
(tr (dm/str "files-download-modal.options." (d/name type) ".message"))]]
(tr (dm/str "dashboard.export.options." (d/name type) ".message"))]]
[:input {:type "radio"
:class (stl/css :option-input)

View File

@@ -19,10 +19,7 @@
[app.main.store :as st]
[app.main.ui.components.code-block :refer [code-block]]
[app.main.ui.components.copy-button :refer [copy-button*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.hooks.resize :refer [use-resize-hook]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.shapes.text.fontfaces :refer [shapes->fonts]]
@@ -263,9 +260,8 @@
[:div {:class (stl/css-case :element-options true
:viewer-code-block (= :viewer from))}
[:div {:class (stl/css :attributes-block)}
[:> button* {:variant "secondary"
:class (stl/css :download-button)
:on-click handle-copy-all-code}
[:button {:class (stl/css :download-button)
:on-click handle-copy-all-code}
"Copy all code"]]
#_[:div.attributes-block
@@ -292,10 +288,10 @@
;; :options [{:label "CSS" :value "css"}]}]
[:div {:class (stl/css :action-btns)}
[:> icon-button* {:variant "ghost"
:aria-label "Expand"
:on-click on-expand
:icon i/code}]
[:button {:class (stl/css :expand-button)
:on-click on-expand}
deprecated-icon/code]
[:> copy-button* {:data copy-css-fn
:class (stl/css :css-copy-btn)
:on-copied on-style-copied}]]]
@@ -322,21 +318,21 @@
:rotated collapsed-markup?)}
deprecated-icon/arrow]]
[:> radio-buttons* {:selected markup-type
:on-change set-markup
:name "listing-style"
:options [{:id "html"
:label "HTML"
:value "html"}
{:id "svg"
:label "SVG"
:value "svg"}]}]
[:& radio-buttons {:selected markup-type
:on-change set-markup
:class (stl/css :code-lang-options)
:wide true
:name "listing-style"}
[:& radio-button {:value "html"
:id :html}]
[:& radio-button {:value "svg"
:id :svg}]]
[:div {:class (stl/css :action-btns)}
[:> icon-button* {:variant "ghost"
:aria-label "Expand"
:on-click on-expand
:icon i/code}]
[:button {:class (stl/css :expand-button)
:on-click on-expand}
deprecated-icon/code]
[:> copy-button* {:data copy-html-fn
:class (stl/css :html-copy-btn)
:on-copied on-markup-copied}]]]

View File

@@ -17,18 +17,16 @@
padding-inline: var(--sp-m);
}
.attributes-block {
display: flex;
flex-direction: column;
row-gap: 12px;
}
.viewer-code-block {
height: calc(100vh - #{deprecated.$s-108}); // TODO: Fix this hardcoded value
}
.download-button {
margin: var(--sp-s) 0;
@extend .button-secondary;
@include deprecated.uppercaseTitleTipography;
height: deprecated.$s-32;
width: 100%;
margin: deprecated.$s-8 0;
}
.code-block {
@@ -75,6 +73,7 @@
gap: deprecated.$s-4;
}
.expand-button,
.css-copy-btn,
.html-copy-btn {
@extend .button-tertiary;
@@ -86,6 +85,9 @@
}
}
.code-lang-options {
max-width: deprecated.$s-108;
}
.code-lang-select {
@include deprecated.uppercaseTitleTipography;
width: deprecated.$s-72;

View File

@@ -14,7 +14,7 @@
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.forms :as fm]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -110,11 +110,11 @@
[:span {:class (stl/css :update-overlay)
:on-click on-image-click} (tr "labels.update")]
[:img {:src photo}]
[:> file-uploader* {:accept "image/jpeg,image/png"
:multi false
:ref input-ref
:on-selected on-file-selected
:data-testid "profile-image-input"}]]]))
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:ref input-ref
:on-selected on-file-selected
:data-testid "profile-image-input"}]]]))
;; --- Profile Page

View File

@@ -27,8 +27,9 @@
[okulary.core :as l]
[rumext.v2 :as mf]))
(mf/defc comments-menu*
{::mf/memo true}
(mf/defc comments-menu
{::mf/props :obj
::mf/memo true}
[]
(let [state (mf/deref refs/comments-local)
cmode (:mode state)

View File

@@ -14,13 +14,10 @@
[app.main.data.viewer.shortcuts :as sc]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.exports.assets :refer [progress-widget*]]
[app.main.ui.exports.assets :refer [progress-widget]]
[app.main.ui.formats :as fmt]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.viewer.comments :refer [comments-menu*]]
[app.main.ui.viewer.comments :refer [comments-menu]]
[app.main.ui.viewer.interactions :refer [flows-menu* interactions-menu*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
@@ -36,12 +33,20 @@
[]
(modal/show! :login-register {}))
(mf/defc zoom-widget*
{::mf/memo true}
[{:keys [zoom on-increase on-decrease on-zoom-reset on-fullscreen on-zoom-fit on-zoom-fill]}]
(let [open* (mf/use-state false)
open? (deref open*)
(mf/defc zoom-widget
{::mf/memo true
::mf/props :obj}
[{:keys [zoom
on-increase
on-decrease
on-zoom-reset
on-fullscreen
on-zoom-fit
on-zoom-fill]
:as props}]
(let [open* (mf/use-state false)
open? (deref open*)
open-dropdown
(mf/use-fn
(fn [event]
@@ -70,7 +75,7 @@
[:div {:class (stl/css-case :zoom-widget true
:selected open?)
:on-click (if open? close-dropdown open-dropdown)
:on-click open-dropdown
:title (tr "workspace.header.zoom")}
[:span {:class (stl/css :label)} (fmt/format-percent zoom)]
[:& dropdown {:show open?
@@ -78,18 +83,18 @@
[:ul {:class (stl/css :dropdown)}
[:li {:class (stl/css :basic-zoom-bar)}
[:span {:class (stl/css :zoom-btns)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "shortcuts.decrease-zoom")
:on-click on-decrease
:icon i/remove}]
[:p {:class (stl/css :zoom-text)}
[:button {:class (stl/css :zoom-btn)
:on-click on-decrease}
[:span {:class (stl/css :zoom-icon)}
deprecated-icon/remove-icon]]
[:p {:class (stl/css :zoom-text)}
(fmt/format-percent zoom)]
[:> icon-button* {:variant "ghost"
:aria-label (tr "shortcuts.increase-zoom")
:on-click on-increase
:icon i/add}]]
[:> button* {:variant "ghost"
:on-click on-zoom-reset}
[:button {:class (stl/css :zoom-btn)
:on-click on-increase}
[:span {:class (stl/css :zoom-icon)}
deprecated-icon/add]]]
[:button {:class (stl/css :reset-btn)
:on-click on-zoom-reset}
(tr "workspace.header.reset-zoom")]]
[:li {:class (stl/css :zoom-option)
@@ -114,7 +119,7 @@
[:span {:class (stl/css :shortcut-key)
:key (dm/str "zoom-fullscreen-" sc)} sc])]]]]]))
(mf/defc header-options*
(mf/defc header-options
[{:keys [section zoom page file index permissions interactions-mode share]}]
(let [fullscreen? (mf/deref fullscreen-ref)
@@ -154,7 +159,6 @@
handle-zoom-fit
(mf/use-fn
#(st/emit! dv/zoom-to-fit))]
(mf/with-effect [permissions share]
(when (and
(:in-team permissions)
@@ -163,7 +167,7 @@
(open-share-dialog)))
[:div {:class (stl/css :options-zone)}
[:> progress-widget*]
[:& progress-widget]
(case section
:interactions [:*
@@ -171,41 +175,40 @@
[:> flows-menu* {:page page :index index}])
[:> interactions-menu*
{:interactions-mode interactions-mode}]]
:comments [:> comments-menu*]
:comments [:& comments-menu]
[:div {:class (stl/css :view-options)}])
[:> zoom-widget* {:zoom zoom
:on-increase handle-increase
:on-decrease handle-decrease
:on-zoom-reset handle-zoom-reset
:on-zoom-fill handle-zoom-fill
:on-zoom-fit handle-zoom-fit
:on-fullscreen toggle-fullscreen}]
[:& zoom-widget
{:zoom zoom
:on-increase handle-increase
:on-decrease handle-decrease
:on-zoom-reset handle-zoom-reset
:on-zoom-fill handle-zoom-fill
:on-zoom-fit handle-zoom-fit
:on-fullscreen toggle-fullscreen}]
(when (:in-team permissions)
[:> icon-button* {:variant "ghost"
:aria-label (tr "viewer.header.edit-in-workspace")
:on-click go-to-workspace
:icon i/curve}])
[:span {:on-click go-to-workspace
:class (stl/css :edit-btn)}
deprecated-icon/curve])
[:> icon-button* {:variant "ghost"
:aria-pressed fullscreen?
:aria-label (tr "viewer.header.fullscreen")
:on-click toggle-fullscreen
:icon i/expand}]
[:span {:title (tr "viewer.header.fullscreen")
:class (stl/css-case :fullscreen-btn true
:selected fullscreen?)
:on-click toggle-fullscreen}
deprecated-icon/expand]
(when (:in-team permissions)
[:> button* {:variant "primary"
:class (stl/css :share-btn)
:on-click open-share-dialog}
[:button {:on-click open-share-dialog
:class (stl/css :share-btn)}
(tr "labels.share")])
(when-not (:is-logged permissions)
[:span {:on-click open-login-dialog
:class (stl/css :go-log-btn)} (tr "labels.log-or-sign")])]))
(mf/defc header-sitemap*
[{:keys [project file page frame toggle-thumbnails]}]
(mf/defc header-sitemap
[{:keys [project file page frame toggle-thumbnails] :as props}]
(let [project-name (:name project)
file-name (:name file)
page-name (:name page)
@@ -314,44 +317,44 @@
:pointer-events (when-not (:in-team permissions) "none")}}
penpot-logo-icon]
[:> header-sitemap* {:project project
:file file
:page page
:frame frame
:toggle-thumbnails toggle-thumbnails
:index index}]]
[:& header-sitemap {:project project
:file file
:page page
:frame frame
:toggle-thumbnails toggle-thumbnails
:index index}]]
[:div {:class (stl/css :mode-zone)}
[:> icon-button* {:variant "ghost"
:aria-pressed (= section :interactions)
:aria-label (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))
:data-value "interactions"
:on-click navigate
:icon i/play}]
[:button {:on-click navigate
:data-value "interactions"
:class (stl/css-case :mode-zone-btn true
:selected (= section :interactions))
:title (tr "viewer.header.interactions-section" (sc/get-tooltip :open-interactions))}
deprecated-icon/play]
(when (or (:in-team permissions)
(= (:who-comment permissions) "all"))
[:> icon-button* {:variant "ghost"
:aria-pressed (= section :comments)
:aria-label (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))
:data-value "comments"
:on-click navigate
:icon i/comments}])
[:button {:on-click navigate
:data-value "comments"
:class (stl/css-case :mode-zone-btn true
:selected (= section :comments))
:title (tr "viewer.header.comments-section" (sc/get-tooltip :open-comments))}
deprecated-icon/comments])
(when (or (:in-team permissions)
(and (= (:type permissions) :share-link)
(= (:who-inspect permissions) "all")))
[:> icon-button* {:variant "ghost"
:aria-pressed (= section :inspect)
:aria-label (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect))
:on-click go-to-inspect
:icon i/code}])]
[:button {:on-click go-to-inspect
:class (stl/css-case :mode-zone-btn true
:selected (= section :inspect))
:title (tr "viewer.header.inspect-section" (sc/get-tooltip :open-inspect))}
deprecated-icon/code])]
[:> header-options* {:section section
:permissions permissions
:page page
:file file
:index index
:zoom zoom
:interactions-mode interactions-mode
:share share}]]))
[:& header-options {:section section
:permissions permissions
:page page
:file file
:index index
:zoom zoom
:interactions-mode interactions-mode
:share share}]]))

View File

@@ -12,7 +12,7 @@
grid-column: 1 / span 1;
grid-row: 1 / span 1;
display: grid;
grid-template-columns: 1fr auto 1fr;
grid-template-columns: 1fr deprecated.$s-92 1fr;
justify-content: space-between;
align-items: center;
height: deprecated.$s-48;
@@ -130,9 +130,23 @@
// SECTION BUTTONS
.mode-zone {
display: flex;
flex-direction: row;
gap: var(--sp-xs);
@include deprecated.flexRow;
height: 100%;
}
.mode-zone-btn {
@extend .button-tertiary;
@include deprecated.flexCenter;
height: deprecated.$s-32;
width: deprecated.$s-28;
padding: 0;
svg {
@extend .button-icon;
}
}
.selected {
@extend .button-icon-selected;
}
// OPTION AREA
@@ -151,8 +165,33 @@
cursor: pointer;
}
.fullscreen-btn {
@extend .button-tertiary;
@include deprecated.flexCenter;
height: deprecated.$s-32;
width: deprecated.$s-28;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.share-btn {
margin-left: var(--sp-xs);
@extend .button-primary;
height: deprecated.$s-32;
min-width: deprecated.$s-72;
margin-left: deprecated.$s-4;
}
.edit-btn {
@extend .button-tertiary;
@include deprecated.flexCenter;
height: deprecated.$s-32;
width: deprecated.$s-28;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.go-log-btn {
@@ -206,15 +245,43 @@
display: flex;
}
.zoom-btn {
@extend .button-tertiary;
height: deprecated.$s-28;
width: deprecated.$s-28;
border-radius: deprecated.$br-8;
.zoom-icon {
@include deprecated.flexCenter;
width: deprecated.$s-24;
height: deprecated.$s-32;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
&:hover {
.zoom-icon svg {
stroke: var(--button-tertiary-foreground-color-hover);
}
}
}
.zoom-text {
@include deprecated.flexCenter;
height: 100%;
min-width: deprecated.$s-48;
min-width: deprecated.$s-64;
padding: 0;
margin: 0 deprecated.$s-2;
color: var(--modal-title-foreground-color);
}
.reset-btn {
@extend .button-tertiary;
color: var(--button-tertiary-foreground-color-hover);
height: deprecated.$s-28;
border-radius: deprecated.$br-8;
}
.zoom-option {
@extend .menu-item-base;
.shortcuts {

View File

@@ -20,9 +20,6 @@
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.clipboard :as clipboard]
[app.util.dom :as dom]
@@ -174,11 +171,10 @@
[:div {:class (stl/css :share-link-header)}
[:h2 {:class (stl/css :share-link-title)}
(tr "common.share-link.title")]
[:> icon-button* {:variant "ghost"
:class (stl/css :modal-close-button)
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}]]
[:button {:class (stl/css :modal-close-button)
:on-click on-close
:title (tr "labels.close")}
deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :share-link-section)}
(when (and (not confirm?) (some? current-link))
@@ -189,10 +185,10 @@
:placeholder (tr "common.share-link.placeholder")
:read-only true}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "viewer.header.share.copy-link")
:on-click copy-link
:icon i/clipboard}]])
[:button {:class (stl/css :copy-button)
:title (tr "viewer.header.share.copy-link")
:on-click copy-link}
deprecated-icon/clipboard]])
[:div {:class (stl/css :hint-wrapper)}
(when (not ^boolean confirm?)
@@ -203,22 +199,28 @@
[:div {:class (stl/css :description)}
(tr "common.share-link.confirm-deletion-link-description")]
[:div {:class (stl/css :actions)}
[:> button* {:variant "secondary"
:on-click #(reset! confirm* false)}
(tr "labels.cancel")]
[:> button* {:variant "destructive"
:on-click delete-link}
(tr "common.share-link.destroy-link")]]]
[:input {:type "button"
:class (stl/css :button-cancel)
:on-click #(reset! confirm* false)
:value (tr "labels.cancel")}]
[:input {:type "button"
:class (stl/css :button-danger)
:on-click delete-link
:value (tr "common.share-link.destroy-link")}]]]
(some? current-link)
[:> button* {:variant "destructive"
:on-click try-delete-link}
(tr "common.share-link.destroy-link")]
[:input
{:type "button"
:class (stl/css :button-danger)
:on-click try-delete-link
:value (tr "common.share-link.destroy-link")}]
:else
[:> button* {:variant "primary"
:on-click create-link}
(tr "common.share-link.get-link")])]]
[:input
{:type "button"
:class (stl/css :button-active)
:on-click create-link
:value (tr "common.share-link.get-link")}])]]
(when (not ^boolean confirm?)
@@ -303,7 +305,6 @@
:options [{:value "team" :label (tr "common.share-link.team-members")}
{:value "all" :label (tr "common.share-link.all-users")}]
:on-change on-comment-change}]]]
[:div {:class (stl/css :inspect-mode)}
[:div {:class (stl/css :subtitle)}
(tr "common.share-link.permissions-can-inspect")]
@@ -314,3 +315,6 @@
:options [{:value "team" :label (tr "common.share-link.team-members")}
{:value "all" :label (tr "common.share-link.all-users")}]
:on-change on-inspect-change}]]]])])]]]))

View File

@@ -30,9 +30,7 @@
}
.modal-close-button {
position: absolute;
top: var(--sp-s);
right: var(--sp-s);
@extend .modal-close-btn-base;
}
.modal-content {
@@ -76,6 +74,18 @@
}
}
.copy-button {
@extend .button-secondary;
@include deprecated.flexRow;
gap: deprecated.$s-8;
height: deprecated.$s-32;
width: deprecated.$s-28;
svg {
@extend .button-icon;
stroke: var(--icon-foreground-hover);
}
}
.description {
@include deprecated.bodySmallTypography;
color: var(--modal-text-foreground-color);
@@ -87,6 +97,18 @@
justify-content: flex-end;
}
.button-active {
@extend .modal-accept-btn;
}
.button-cancel {
@extend .modal-cancel-btn;
}
.button-danger {
@extend .modal-danger-btn;
}
.permissions-section {
@include deprecated.flexColumn;
gap: deprecated.$s-8;

View File

@@ -36,7 +36,6 @@
[app.main.ui.workspace.tokens.import]
[app.main.ui.workspace.tokens.import.modal]
[app.main.ui.workspace.tokens.management.forms.modals]
[app.main.ui.workspace.tokens.remapping-modal]
[app.main.ui.workspace.tokens.settings]
[app.main.ui.workspace.tokens.themes.create-modal]
[app.main.ui.workspace.viewport :refer [viewport*]]

View File

@@ -25,11 +25,10 @@
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.radio-buttons :refer [radio-buttons radio-button]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.hooks :as hooks]
@@ -416,25 +415,24 @@
:on-change handle-change-mode}]])
(when (and (= origin :sidebar) show-tokens? token-color)
[:> radio-buttons* {:selected color-style
:on-change toggle-token-color
:name "color-style"
:options [{:id "swap-opt-list"
:icon i/swatches
:label (tr "labels.color")
:value :direct-color}
{:id "swap-opt-grid"
:icon i/tokens
:label (tr "workspace.colorpicker.color-tokens")
:value :token-color}]}])]
[:& radio-buttons {:selected color-style
:on-change toggle-token-color
:name "color-style"}
[:& radio-button {:icon i/swatches
:value :direct-color
:title (tr "labels.color")
:id "opt-color"}]
[:& radio-button {:icon i/tokens
:value :token-color
:title (tr "workspace.colorpicker.color-tokens")
:id "opt-token-color"}]])]
(when (and (not= selected-mode :image)
(= color-style :direct-color))
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.colorpicker.get-color")
:aria-pressed picking-color?
:on-click handle-click-picker
:icon i/picker}])
[:button {:class (stl/css-case :picker-btn true
:selected picking-color?)
:on-click handle-click-picker}
deprecated-icon/picker])
(when (= color-style :token-color)
[:div {:class (stl/css :token-color-title)}
@@ -485,11 +483,12 @@
:aria-label (tr "media.choose-image")
:on-click on-fill-image-click}
(tr "media.choose-image")
[:> file-uploader* {:input-id "fill-image-upload"
:accept "image/jpeg,image/png"
:multi false
:ref fill-image-ref
:on-selected on-fill-image-selected}]]])
[:& file-uploader
{:input-id "fill-image-upload"
:accept "image/jpeg,image/png"
:multi false
:ref fill-image-ref
:on-selected on-fill-image-selected}]]])
[:*
[:div {:class (stl/css :colorpicker-tabs)}

View File

@@ -46,6 +46,52 @@
width: px2rem(68);
}
// TODO: change to DS button component
.picker-btn {
display: flex;
justify-content: center;
align-items: center;
border: none;
background: none;
cursor: pointer;
border-radius: $br-8;
background-color: transparent;
border: $b-1 solid transparent;
height: var(--sp-xl);
width: var(--sp-xl);
border-radius: $br-4;
padding: 0;
margin-top: var(--sp-xs);
svg {
@extend .button-icon;
stroke: var(--button-tertiary-foreground-color-rest);
}
&:hover {
svg {
stroke: var(--button-tertiary-foreground-color-focus);
}
}
&:focus,
&:focus-visible {
outline: none;
svg {
stroke: var(--button-secondary-foreground-color-hover);
}
}
&:active {
outline: none;
border: $b-1 solid transparent;
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
&.selected {
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
}
.gradient-buttons {
display: flex;
align-items: center;

View File

@@ -152,6 +152,7 @@
(when path-set
(ptk/data-event :expand-token-sets {:paths path-set}))
(dwtl/set-selected-token-set-id id)
(dwtl/set-token-type-section-open :color true)
(let [{:keys [modal title]} (get dwta/token-properties :color)
window-size (dom/get-window-size)
left-sidebar (dom/get-element "left-sidebar-aside")

View File

@@ -30,7 +30,6 @@
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
@@ -45,6 +44,12 @@
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(def ^:private close-icon
(deprecated-icon/icon-xref :close (stl/css :close-icon)))
(def ^:private add-icon
(deprecated-icon/icon-xref :add (stl/css :add-icon)))
(defn- get-library-summary
"Given a library data return a summary representation of this library"
[data]
@@ -163,10 +168,12 @@
[:div {:class (stl/css :sample-library-item)
:key (dm/str id)}
[:div {:class (stl/css :sample-library-item-name)} (:name library)]
[:> button* {:variant "secondary"
:on-click import-library
:disabled (some? importing?)}
(if (= importing? id) (tr "labels.adding") (tr "labels.add"))]]))
[:input {:class (stl/css-case :sample-library-button true
:sample-library-add (nil? importing?)
:sample-library-adding (some? importing?))
:type "button"
:value (if (= importing? id) (tr "labels.adding") (tr "labels.add"))
:on-click import-library}]]))
(defn- empty-library?
"Check if currentt library summary has elements or not"
@@ -315,12 +322,14 @@
[:> library-description* {:summary summary}]]]
(if ^boolean is-shared
[:> button* {:variant "secondary"
:on-click unpublish}
(tr "common.unpublish")]
[:> button* {:variant "primary"
:on-click publish}
(tr "common.publish")])]
[:input {:class (stl/css :item-unpublish)
:type "button"
:value (tr "common.unpublish")
:on-click unpublish}]
[:input {:class (stl/css :item-publish)
:type "button"
:value (tr "common.publish")
:on-click publish}])]
(for [{:keys [id name data connected-to connected-to-names] :as library} linked-libraries]
(let [disabled? (some #(contains? linked-libraries-ids %) connected-to)]
@@ -368,11 +377,12 @@
(let [summary (-> (:library-summary library)
(adapt-backend-summary))]
[:> library-description* {:summary summary}])]]
[:> icon-button* {:variant "secondary"
:aria-label (tr "workspace.libraries.shared-library-btn")
:icon i/add
:data-library-id (dm/str id)
:on-click link-library}]])]
[:button {:class (stl/css :item-button-shared)
:data-library-id (dm/str id)
:title (tr "workspace.libraries.shared-library-btn")
:on-click link-library}
add-icon]])]
(when (empty? shared-libraries)
[:div {:class (stl/css :section-list-empty)}
@@ -637,13 +647,11 @@
:on-click close-dialog-outside
:data-testid "libraries-modal"}
[:div {:class (stl/css :modal-dialog)}
[:> icon-button* {:variant "ghost"
:class (stl/css :close-btn)
:icon i/close
:aria-label (tr "labels.close")
:data-testid "close-libraries"
:on-click close-dialog}]
[:button {:class (stl/css :close-btn)
:on-click close-dialog
:aria-label (tr "labels.close")
:data-testid "close-libraries"}
close-icon]
[:div {:class (stl/css :modal-title)}
(tr "workspace.libraries.libraries")]

View File

@@ -33,7 +33,7 @@
background-color: var(--modal-background-color);
border: $b-2 solid var(--modal-border-color);
display: grid;
grid-template-rows: 0 auto 1fr;
grid-template-rows: auto 1fr;
min-width: $sz-364;
min-height: $sz-192;
height: $sz-520;
@@ -42,10 +42,21 @@
max-width: $sz-712;
}
// TODO: Remove this extended creating modal component
.close-btn {
position: absolute;
top: var(--sp-s);
right: var(--sp-s);
@extend .modal-close-btn-base;
}
.close-icon {
display: flex;
justify-content: center;
align-items: center;
height: $sz-16;
width: $sz-16;
color: transparent;
fill: none;
stroke-width: $b-1;
stroke: var(--icon-foreground);
}
.modal-title {
@@ -109,6 +120,46 @@
height: fit-content;
}
.item-publish,
.item-unpublish {
// TODO: remove this extended by using DS button component
@extend .button-primary;
@include t.use-typography("headline-small");
height: $sz-32;
min-width: px2rem(92);
padding: var(--sp-s) var(--sp-xxl);
margin: 0;
border-radius: $br-8;
}
.item-unpublish {
// TODO: remove this extended by using DS button component
@extend .button-secondary;
}
.item-button,
.item-button-shared {
// TODO: remove this extended by using DS button component
@extend .button-secondary;
height: $sz-32;
width: $sz-32;
margin-inline-start: var(--sp-xxs);
padding: var(--sp-s);
}
.detach-icon,
.add-icon {
display: flex;
justify-content: center;
align-items: center;
height: $sz-16;
width: $sz-16;
color: transparent;
fill: none;
stroke-width: $b-1;
stroke: var(--icon-foreground);
}
.section-list-shared {
max-height: px2rem(272);
}
@@ -119,6 +170,26 @@
color: var(--title-foreground-color);
}
.search-icon {
display: flex;
justify-content: center;
align-items: center;
width: px2rem(20);
padding: 0 0 0 var(--sp-s);
svg {
display: flex;
justify-content: center;
align-items: center;
color: transparent;
fill: none;
height: px2rem(12);
width: px2rem(12);
stroke-width: 1.33px;
stroke: var(--icon-foreground);
}
}
// empty state
.section-list-empty {
display: grid;
@@ -357,3 +428,24 @@
text-overflow: ellipsis;
max-width: px2rem(232);
}
// TODO: Remove this extended using a DS component
.sample-library-add {
@extend .button-secondary;
}
// TODO: Remove this extended using a DS component
.sample-library-adding {
@extend .button-disabled;
}
.sample-library-button {
@include t.use-typography("headline-small");
height: $sz-32;
width: px2rem(80);
margin: 0;
border-radius: $br-8;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}

View File

@@ -853,9 +853,8 @@
[:*
[:> icon-button* {:variant "ghost"
:aria-pressed show-menu?
:aria-label (tr "shortcut-subsection.main-menu")
:on-click (if show-menu? close-all-menus open-menu)
:on-click open-menu
:icon i/menu}]
[:> dropdown-menu* {:show show-menu?

View File

@@ -18,10 +18,9 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.hooks :as h]
[app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.color-palette :refer [color-palette*]]
[app.main.ui.workspace.color-palette-ctx-menu :refer [color-palette-ctx-menu*]]
[app.main.ui.workspace.text-palette :refer [text-palette]]
@@ -179,27 +178,27 @@
[:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true
:hidden-bts hide-palettes?))}
[:li {:class (stl/css :palette-item)}
[:> icon-button* {:variant "ghost"
:aria-pressed (some? color-palette?)
:aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
:on-click on-select-color-palette
:icon i/drop}]]
[:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
:aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
:class (stl/css-case :palette-btn true
:selected color-palette?)
:on-click on-select-color-palette}
deprecated-icon/drop-icon]]
[:li {:class (stl/css :palette-item)}
[:> icon-button* {:variant "ghost"
:aria-pressed (some? text-palette?)
:aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
:on-click on-select-text-palette
:icon i/text-palette}]]]
[:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
:aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
:class (stl/css-case :palette-btn true
:selected text-palette?)
:on-click on-select-text-palette}
deprecated-icon/text-palette]]]
(if any-palette?
[:*
[:div {:class (stl/css :menu-btn)}
[:> icon-button* {:variant "ghost"
:aria-pressed show-menu?
:aria-label (tr "labels.options")
:on-click #(swap! state* update :show-menu not)
:icon i/menu}]]
[:button {:class (stl/css :palette-actions)
:on-click #(swap! state* update :show-menu not)}
deprecated-icon/menu]
[:div {:class (stl/css :palette)
:ref container}
(when text-palette?

View File

@@ -49,6 +49,7 @@
&.wide {
width: 100%;
}
.resize-area {
grid-area: resize;
height: deprecated.$s-8;
@@ -71,22 +72,49 @@
&.small-palette {
display: flex;
}
.palette-item {
@include deprecated.flexCenter;
border-radius: deprecated.$br-8;
opacity: deprecated.$op-10;
transition: opacity 1s ease;
.palette-btn {
@extend .button-tertiary;
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-8;
background-clip: padding-box;
padding: 0;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
&.selected {
@extend .button-icon-selected;
}
}
}
}
.palette-actions {
@extend .button-tertiary;
grid-area: actions;
height: calc(var(--height) - deprecated.$s-16);
width: deprecated.$s-32;
padding: 0;
margin-left: deprecated.$s-4;
border-radius: deprecated.$br-8;
background-color: var(--palette-background-color);
z-index: deprecated.$z-index-2;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.palette {
grid-area: palette;
width: 100%;
min-width: 0;
}
.palette-item {
display: flex;
align-items: center;
}
}
.menu-btn {
display: flex;
align-items: center;
margin-left: var(--sp-s);
}
.handler {

View File

@@ -29,7 +29,7 @@
:style {:background-color color}
:src (cfg/resolve-profile-photo-url profile)}]]))
(mf/defc active-sessions*
(mf/defc active-sessions
{::mf/memo true}
[]
(let [profiles (mf/deref refs/profiles)

View File

@@ -20,19 +20,23 @@
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.context :as ctx]
[app.main.ui.dashboard.team]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.exports.assets :refer [progress-widget*]]
[app.main.ui.exports.assets :refer [progress-widget]]
[app.main.ui.formats :as fmt]
[app.main.ui.workspace.presence :refer [active-sessions*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.presence :refer [active-sessions]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ref:persistence-status
(l/derived :status refs/persistence))
;; --- Zoom Widget
(mf/defc zoom-widget-workspace*
(mf/defc zoom-widget-workspace
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}]
@@ -68,12 +72,11 @@
zoom (fmt/format-percent zoom {:precision 0})]
[:*
[:div {:on-click (if open? close-dropdown open-dropdown)
[:div {:on-click open-dropdown
:class (stl/css-case :zoom-widget true
:selected open?)
:title (tr "workspace.header.zoom")}
[:span {:class (stl/css :label)} zoom]]
[:& dropdown {:show open? :on-close close-dropdown}
[:ul {:class (stl/css :dropdown)}
[:li {:class (stl/css :basic-zoom-bar)}
@@ -87,10 +90,9 @@
:aria-label (tr "shortcuts.increase-zoom")
:on-click on-increase
:icon i/add}]]
[:> button* {:variant "ghost"
:on-click on-zoom-reset}
[:button {:class (stl/css :reset-btn)
:on-click on-zoom-reset}
(tr "workspace.header.reset-zoom")]]
[:li {:class (stl/css :zoom-option)
:on-click on-zoom-fit}
(tr "workspace.header.zoom-fit-all")
@@ -98,7 +100,6 @@
(for [sc (scd/split-sc (sc/get-tooltip :fit-all))]
[:span {:class (stl/css :shortcut-key)
:key (str "zoom-fit-" sc)} sc])]]
[:li {:class (stl/css :zoom-option)
:on-click on-zoom-selected}
(tr "workspace.header.zoom-selected")
@@ -197,43 +198,51 @@
[:div {:class (stl/css :workspace-header-right)}
[:div {:class (stl/css :users-section)}
[:> active-sessions*]]
[:& active-sessions]]
[:> progress-widget*]
[:& progress-widget]
[:div {:class (stl/css :separator)}]
[:div {:class (stl/css :zoom-section)}
[:> zoom-widget-workspace* {:zoom zoom
:on-increase on-increase
:on-decrease on-decrease
:on-zoom-reset on-zoom-reset
:on-zoom-fit on-zoom-fit
:on-zoom-selected on-zoom-selected}]]
[:& zoom-widget-workspace
{:zoom zoom
:on-increase on-increase
:on-decrease on-decrease
:on-zoom-reset on-zoom-reset
:on-zoom-fit on-zoom-fit
:on-zoom-selected on-zoom-selected}]]
[:div {:class (stl/css :comments-button-wrapper)}
[:> icon-button* {:variant "ghost"
:aria-pressed (= selected-drawtool :comments)
:aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
:on-click toggle-comments
:icon i/comments}]
(when ^boolean has-unread-comments?
[:div {:class (stl/css :unread)}])]
[:div {:class (stl/css :comments-section)}
[:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
:aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
:class (stl/css-case :comments-btn true
:selected (= selected-drawtool :comments))
:on-click toggle-comments
:data-tool "comments"
:style {:position "relative"}}
deprecated-icon/comments
(when ^boolean has-unread-comments?
[:div {:class (stl/css :unread)}])]]
(when-not ^boolean read-only?
[:> icon-button* {:variant "ghost"
:aria-pressed (contains? layout :document-history)
:aria-label (tr "workspace.sidebar.history")
:on-click toggle-history
:icon i/history}])
[:div {:class (stl/css :history-section)}
[:button
{:title (tr "workspace.sidebar.history")
:aria-label (tr "workspace.sidebar.history")
:class (stl/css-case :selected (contains? layout :document-history)
:history-button true)
:on-click toggle-history}
deprecated-icon/history]])
(when display-share-button?
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.header.share")
:on-click open-share-dialog
:icon i/to-corner}])
[:a {:class (stl/css :viewer-btn)
:title (tr "workspace.header.share")
:on-click open-share-dialog}
deprecated-icon/share])
[:a {:class (stl/css :viewer-btn)
:title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
:on-click nav-to-viewer}
deprecated-icon/play]]))
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
:on-click nav-to-viewer
:icon i/play}]]))

View File

@@ -11,8 +11,8 @@
justify-content: space-between;
align-items: center;
min-width: deprecated.$s-256;
padding: deprecated.$s-8 deprecated.$s-12;
gap: deprecated.$s-4;
padding: deprecated.$s-8;
gap: deprecated.$s-8;
background-color: var(--panel-background-color);
}
@@ -28,14 +28,19 @@
}
.zoom-widget {
@include deprecated.buttonStyle;
display: flex;
align-items: center;
justify-content: center;
height: deprecated.$s-28;
max-width: deprecated.$s-48;
width: deprecated.$s-48;
height: deprecated.$s-32;
border-radius: deprecated.$br-8;
.label {
@include deprecated.bodySmallTypography;
height: 100%;
padding: deprecated.$s-8 0;
color: var(--button-tertiary-foreground-color-rest);
}
@@ -79,6 +84,13 @@
color: var(--modal-title-foreground-color);
}
.reset-btn {
@extend .button-tertiary;
color: var(--button-tertiary-foreground-color-hover);
height: deprecated.$s-28;
border-radius: deprecated.$br-8;
}
.zoom-option {
@extend .menu-item-base;
@@ -101,11 +113,127 @@
}
}
.comments-button-wrapper {
position: relative;
.comments-btn {
@extend .button-tertiary;
border-radius: deprecated.$br-8;
margin: 0;
height: deprecated.$s-28;
width: deprecated.$s-28;
border: none;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
height: deprecated.$s-16;
width: deprecated.$s-16;
}
&:hover {
background-color: transparent;
border: none;
}
&.selected {
background-color: var(--button-tertiary-background-color-selected);
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
}
.comments-button-unread {
.history-button {
@extend .button-tertiary;
border-radius: deprecated.$br-8;
margin: 0;
height: deprecated.$s-28;
width: deprecated.$s-28;
border: none;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
height: deprecated.$s-16;
width: deprecated.$s-16;
}
&:hover {
background-color: transparent;
border: none;
}
&.selected {
background-color: var(--button-tertiary-background-color-selected);
svg {
stroke: var(--button-tertiary-foreground-color-active);
}
}
}
.persistence-status-widget {
@include deprecated.flexCenter;
width: deprecated.$s-28;
height: deprecated.$s-28;
}
.status-icon {
@include deprecated.flexCenter;
width: deprecated.$s-24;
height: deprecated.$s-24;
margin: 0;
border-radius: deprecated.$br-circle;
svg {
@extend .button-icon;
stroke: var(--status-widget-icon-foreground-color);
}
}
.pending-status {
background-color: var(--status-widget-background-color-warning);
}
.saving-status {
background-color: var(--status-widget-background-color-pending);
svg {
animation: spin-animation 1s infinite;
animation-timing-function: linear;
}
}
.saved-status {
background-color: var(--status-widget-background-color-success);
}
.error-status {
background-color: var(--status-widget-background-color-error);
}
.share-btn,
.viewer-btn {
@extend .button-tertiary;
border-radius: deprecated.$br-8;
margin: 0;
width: deprecated.$s-28;
height: deprecated.$s-28;
border: none;
svg {
@extend .button-icon;
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
}
&:hover {
background-color: transparent;
border: none;
}
}
.unread {
position: absolute;
width: 8px;
height: 8px;

View File

@@ -17,9 +17,8 @@
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.assets.file-library :refer [file-library*]]
[app.util.dom :as dom]
@@ -162,40 +161,43 @@
:id "typographies"
:handler on-section-filter-change}])]
[:article {:class (stl/css :assets-bar)}
[:article {:class (stl/css :assets-bar)}
[:div {:class (stl/css :assets-header)}
(when-not ^boolean read-only?
(if (and (= num-libs 1) (empty? components))
[:> button* {:variant "primary"
:on-click show-libraries-dialog
:data-testid "libraries"}
[:button {:class (stl/css :add-library-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
(tr "workspace.assets.add-library")]
[:> button* {:variant "secondary"
:on-click show-libraries-dialog
:data-testid "libraries"}
[:button {:class (stl/css :libraries-button)
:on-click show-libraries-dialog
:data-testid "libraries"}
(tr "workspace.assets.manage-library")]))
[:div {:class (stl/css :search-wrapper)}
[:> search-bar* {:on-change on-search-term-change
:value term
:placeholder (tr "workspace.assets.search")}
[:> icon-button* {:variant "secondary"
:icon i/filter
:class (stl/css :filter-button)
:aria-pressed menu-open?
:aria-label (tr "workspace.assets.filter")
:on-click on-open-menu}]]
[:button
{:on-click on-open-menu
:title (tr "workspace.assets.filter")
:class (stl/css-case :section-button true
:opened menu-open?)}
deprecated-icon/filter-icon]]
[:> context-menu* {:on-close on-menu-close
:selectable true
:selected section
:show menu-open?
:fixed true
:min-width true
:width size
:top 158
:left 18
:options options}]
[:> context-menu*
{:on-close on-menu-close
:selectable true
:selected section
:show menu-open?
:fixed true
:min-width true
:width size
:top 158
:left 18
:options options}]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.assets.sort")

View File

@@ -17,14 +17,89 @@
padding-top: deprecated.$s-8;
}
.assets-header {
display: flex;
flex-direction: column;
gap: var(--sp-xxs);
.libraries-button {
@extend .button-secondary;
@include deprecated.uppercaseTitleTipography;
gap: deprecated.$s-2;
height: deprecated.$s-32;
width: 100%;
margin-bottom: deprecated.$s-4;
border-radius: deprecated.$s-8;
&:hover {
background-color: var(--button-secondary-background-color-hover);
color: var(--button-secondary-foreground-color-hover);
border: deprecated.$s-1 solid var(--button-secondary-border-color-hover);
}
&:focus {
background-color: var(--button-secondary-background-color-focus);
color: var(--button-secondary-foreground-color-focus);
border: deprecated.$s-1 solid var(--button-secondary-border-color-focus);
}
}
.filter-button {
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
.add-library-button {
@extend .button-primary;
@include deprecated.uppercaseTitleTipography;
gap: deprecated.$s-2;
height: deprecated.$s-32;
width: 100%;
margin-bottom: deprecated.$s-4;
border-radius: deprecated.$s-8;
}
.section-button {
@include deprecated.flexCenter;
@include deprecated.buttonStyle;
height: deprecated.$s-32;
width: deprecated.$s-32;
margin: 0;
border: deprecated.$s-1 solid var(--input-border-color-rest);
border-radius: deprecated.$br-8 deprecated.$br-2 deprecated.$br-2 deprecated.$br-8;
background-color: var(--input-background-color-rest);
svg {
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
}
&:focus {
border: deprecated.$s-1 solid var(--input-border-color-focus);
outline: 0;
background-color: var(--input-background-color-focus);
color: var(--input-foreground-color-focus);
svg {
background-color: var(--input-background-color-focus);
}
}
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-hover);
background-color: var(--input-background-color-hover);
svg {
background-color: var(--input-background-color-hover);
stroke: var(--button-foreground-hover);
}
&:focus {
border: deprecated.$s-1 solid var(--input-border-color-focus);
outline: 0;
background-color: var(--input-background-color-focus);
color: var(--input-foreground-color-focus);
svg {
background-color: var(--input-background-color-focus);
}
}
}
&.opened {
@extend .button-icon-selected;
}
}
.sections-container {
@@ -50,6 +125,10 @@
border-radius: deprecated.$br-8;
}
.section-btn {
@include deprecated.buttonStyle;
}
.assets-header {
padding: 0 0 deprecated.$s-24 deprecated.$s-12;
}

View File

@@ -22,10 +22,10 @@
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.editable-label :refer [editable-label*]]
[app.main.ui.components.file-uploader :refer [file-uploader*]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
@@ -563,27 +563,27 @@
[:> cmm/asset-section-block* {:role :title-button}
(when ^boolean is-open
[:div
[:> radio-buttons* {:selected (if is-listing-thumbs "grid" "list")
:on-change toggle-list-style
:name "listing-style"
:options [{:id "opt-list"
:icon i/view-as-list
:label (tr "workspace.assets.list-view")
:value "list"}
{:id "opt-grid"
:icon i/flex-grid
:label (tr "workspace.assets.grid-view")
:value "grid"}]}]])
[:& radio-buttons {:selected (if is-listing-thumbs "grid" "list")
:on-change toggle-list-style
:name "listing-style"}
[:& radio-button {:icon i/view-as-list
:value "list"
:title (tr "workspace.assets.list-view")
:id "opt-list"}]
[:& radio-button {:icon i/flex-grid
:value "grid"
:title (tr "workspace.assets.grid-view")
:id "opt-grid"}]]])
(when (and (not read-only?) is-local)
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.assets.components.add-component")
:on-click add-component
:icon i/add}
[:> file-uploader* {:accept dwm/accept-image-types
:multi true
:ref input-ref
:on-selected on-file-selected}]])]
[:& file-uploader {:accept dwm/accept-image-types
:multi true
:ref input-ref
:on-selected on-file-selected}]])]
[:> cmm/asset-section-block* {:role :content}
(when ^boolean is-open

View File

@@ -23,7 +23,7 @@
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry*]]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [typography-entry]]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[cuerdas.core :as str]
@@ -113,17 +113,18 @@
:on-drag-over dom/prevent-default
:on-drop on-drop}
[:> typography-entry* {:file-id file-id
:typography typography
:local? local?
:selected? (contains? selected typography-id)
:on-click on-asset-click
:on-change handle-change
:on-context-menu on-context-menu
:editing? editing?
:renaming? renaming?
:focus-name? rename?
:external-open* open*}]
[:& typography-entry
{:file-id file-id
:typography typography
:local? local?
:selected? (contains? selected typography-id)
:on-click on-asset-click
:on-change handle-change
:on-context-menu on-context-menu
:editing? editing?
:renaming? renaming?
:focus-name? rename?
:external-open* open*}]
(when ^boolean dragging?
[:div {:class (stl/css :dragging)}])]))

View File

@@ -291,12 +291,13 @@
:value current-search
:on-clear clear-search-text
:placeholder (tr "workspace.sidebar.layers.search")}
[:> icon-button* {:variant "secondary"
:class (stl/css :filter-button)
:aria-pressed show-menu?
:aria-label (tr "workspace.sidebar.layers.filter")
:on-click on-toggle-filters-click
:icon i/filter}]]
[:button {:on-click on-toggle-filters-click
:class (stl/css-case
:filter-button true
:opened show-menu?
:active active?)}
[:> icon* {:icon-id i/filter}]]]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click toggle-search

View File

@@ -19,7 +19,39 @@
padding: 0 deprecated.$s-12 0 deprecated.$s-8;
gap: deprecated.$s-4;
.filter-button {
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
@include deprecated.flexCenter;
@include deprecated.buttonStyle;
height: deprecated.$s-32;
width: deprecated.$s-32;
margin: 0;
border: deprecated.$s-1 solid var(--color-background-tertiary);
border-radius: deprecated.$br-8 deprecated.$br-2 deprecated.$br-2 deprecated.$br-8;
background-color: var(--color-background-tertiary);
svg {
height: deprecated.$s-16;
width: deprecated.$s-16;
stroke: var(--icon-foreground);
}
&:focus {
border: deprecated.$s-1 solid var(--input-border-color-focus);
outline: 0;
background-color: var(--input-background-color-active);
color: var(--input-foreground-color-active);
svg {
background-color: var(--input-background-color-active);
}
}
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-hover);
background-color: var(--input-background-color-hover);
svg {
background-color: var(--input-background-color-hover);
stroke: var(--button-foreground-hover);
}
}
&.opened {
@extend .button-icon-selected;
}
}
}
}
@@ -111,7 +143,7 @@
.filters-container {
@extend .menu-dropdown;
position: absolute;
left: deprecated.$s-16;
left: deprecated.$s-20;
width: deprecated.$s-192;
.filter-menu-item {
@include deprecated.bodySmallTypography;

View File

@@ -54,7 +54,7 @@
modifiers (dm/get-in modifiers [shape-id :modifiers])
shape (gsh/transform-shape shape modifiers)
props (mf/spread-props props {:shape shape :file-id file-id :page-id page-id :libraries libraries})]
props (mf/spread-props props {:shape shape :file-id file-id :page-id page-id})]
(case shape-type
:frame [:> frame/options* props]

View File

@@ -12,7 +12,7 @@
[app.main.data.workspace.drawing :as dwd]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
@@ -95,15 +95,15 @@
(when preset-match
[:span {:class (stl/css :check-icon)} deprecated-icon/tick])])))]]]
[:> radio-buttons* {:class (stl/css :radio-buttons)
:selected (or (d/name orientation) "")
:on-change on-orientation-change
:name "frame-orientation"
:options [{:id "size-vertical"
:icon i/size-vertical
:label (tr "workspace.options.orientation.vertical")
:value "vertical"}
{:id "size-horizontal"
:icon i/size-horizontal
:label (tr "workspace.options.orientation.horizontal")
:value "horizontal"}]}]]))
[:& radio-buttons {:selected (or (d/name orientation) "")
:on-change on-orientation-change
:name "frame-orientation"
:wide true
:class (stl/css :radio-buttons)}
[:& radio-button {:icon i/size-vertical
:value "vertical"
:id "size-vertical"}]
[:& radio-button {:icon i/size-horizontal
:value "horizontal"
:id "size-horizontal"}]]]))

View File

@@ -10,8 +10,7 @@
[app.main.data.workspace :as dw]
[app.main.data.workspace.shortcuts :as sc]
[app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -43,59 +42,68 @@
(when-not (and disabled-align disabled-distribute)
[:div {:class (stl/css :align-options)}
[:div {:class (stl/css :align-group-horizontal)}
[:> icon-button* {:variant "ghost"
:icon i/align-left
:aria-label (tr "workspace.align.hleft" (sc/get-tooltip :align-left))
:on-click align-objects
:data-value "hleft"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.hleft" (sc/get-tooltip :align-left))
:data-value "hleft"
:on-click align-objects}
deprecated-icon/align-left]
[:> icon-button* {:variant "ghost"
:icon i/align-horizontal-center
:aria-label (tr "workspace.align.hcenter" (sc/get-tooltip :align-hcenter))
:on-click align-objects
:data-value "hcenter"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.hcenter" (sc/get-tooltip :align-hcenter))
:data-value "hcenter"
:on-click align-objects}
deprecated-icon/align-horizontal-center]
[:> icon-button* {:variant "ghost"
:icon i/align-right
:aria-label (tr "workspace.align.hright" (sc/get-tooltip :align-right))
:on-click align-objects
:data-value "hright"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.hright" (sc/get-tooltip :align-right))
:data-value "hright"
:on-click align-objects}
deprecated-icon/align-right]
[:> icon-button* {:variant "ghost"
:icon i/distribute-horizontally
:aria-label (tr "workspace.align.hdistribute" (sc/get-tooltip :h-distribute))
:on-click distribute-objects
:data-value "horizontal"
:disabled disabled-distribute}]]
[:button {:class (stl/css-case :align-button true
:disabled disabled-distribute)
:disabled disabled-distribute
:title (tr "workspace.align.hdistribute" (sc/get-tooltip :h-distribute))
:data-value "horizontal"
:on-click distribute-objects}
deprecated-icon/distribute-horizontally]]
[:div {:class (stl/css :align-group-vertical)}
[:> icon-button* {:variant "ghost"
:icon i/align-top
:aria-label (tr "workspace.align.vtop" (sc/get-tooltip :align-top))
:on-click align-objects
:data-value "vtop"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.vtop" (sc/get-tooltip :align-top))
:data-value "vtop"
:on-click align-objects}
deprecated-icon/align-top]
[:> icon-button* {:variant "ghost"
:icon i/align-vertical-center
:aria-label (tr "workspace.align.vcenter" (sc/get-tooltip :align-vcenter))
:on-click align-objects
:data-value "vcenter"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.vcenter" (sc/get-tooltip :align-vcenter))
:data-value "vcenter"
:on-click align-objects}
deprecated-icon/align-vertical-center]
[:> icon-button* {:variant "ghost"
:icon i/align-bottom
:aria-label (tr "workspace.align.vbottom" (sc/get-tooltip :align-bottom))
:on-click align-objects
:data-value "vbottom"
:disabled disabled-align}]
[:button {:class (stl/css-case :align-button true
:disabled disabled-align)
:disabled disabled-align
:title (tr "workspace.align.vbottom" (sc/get-tooltip :align-bottom))
:data-value "vbottom"
:on-click align-objects}
deprecated-icon/align-bottom]
[:button {:title (tr "workspace.align.vdistribute" (sc/get-tooltip :v-distribute))
:class (stl/css-case :align-button true
:disabled disabled-distribute)
:disabled disabled-distribute
:data-value "vertical"
:on-click distribute-objects}
deprecated-icon/distribute-vertical-spacing]]])))
[:> icon-button* {:variant "ghost"
:icon i/distribute-vertical-spacing
:aria-label (tr "workspace.align.vdistribute" (sc/get-tooltip :v-distribute))
:on-click distribute-objects
:data-value "vertical"
:disabled disabled-distribute}]]])))

View File

@@ -4,10 +4,12 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
.align-options {
@include sidebar.option-grid-structure;
height: deprecated.$s-32;
}
.align-group-horizontal,
.align-group-vertical {
@@ -24,3 +26,27 @@
.align-group-vertical {
grid-column: 5 / span 4;
}
.align-button {
@extend .button-tertiary;
height: deprecated.$s-32;
width: deprecated.$s-32;
padding: 0;
border-radius: deprecated.$br-8;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
&.disabled {
cursor: default;
svg {
stroke: var(--button-foreground-color-disabled);
}
&:hover {
background-color: var(--panel-background-color);
svg {
stroke: var(--button-foreground-color-disabled);
}
}
}
}

View File

@@ -15,6 +15,7 @@
[app.main.ui.components.title-bar :refer [title-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -102,12 +103,10 @@
[:div {:class (stl/css-case :first-row true
:hidden hidden?)}
[:div {:class (stl/css :blur-info)}
[:> icon-button* {:variant "secondary"
:class (stl/css :show-more)
:aria-label (tr "labels.options")
:aria-pressed more-options?
:on-click toggle-more-options
:icon i/menu}]
[:button {:class (stl/css-case :show-more true
:selected more-options?)
:on-click toggle-more-options}
deprecated-icon/menu]
[:span {:class (stl/css :label)}
(tr "workspace.options.blur-options.title")]]
[:div {:class (stl/css :actions)}

View File

@@ -37,7 +37,21 @@
border-radius: deprecated.$br-8;
background-color: var(--input-details-color);
.show-more {
@extend .button-secondary;
height: deprecated.$s-32;
width: deprecated.$s-28;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
box-sizing: border-box;
border: deprecated.$s-1 solid var(--button-secondary-background-color-rest);
svg {
@extend .button-icon;
}
&.selected {
background-color: var(--button-radio-background-color-active);
svg {
stroke: var(--button-radio-foreground-color-active);
}
}
}
.label {
@include deprecated.bodySmallTypography;

View File

@@ -15,12 +15,15 @@
[app.main.data.workspace.shortcuts :as sc]
[app.main.features :as features]
[app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private flatten-icon
(deprecated-icon/icon-xref :boolean-flatten (stl/css :flatten-icon)))
(mf/defc bool-options*
[{:keys [total-selected shapes shapes-with-children]}]
(let [head (first shapes)
@@ -67,40 +70,41 @@
(st/emit! (dwb/change-bool-type head-id bool-type)))))))
flatten-objects
(mf/use-fn
#(st/emit! (dwps/convert-selected-to-path)))]
(mf/use-fn #(st/emit! (dwps/convert-selected-to-path)))]
(when (not (and disabled-bool-btns disabled-flatten))
[:div {:class (stl/css :boolean-options)}
[:div {:class (stl/css :boolean-group)}
[:> radio-buttons* {:class (stl/css :boolean-radio-btn)
:variant "ghost"
:selected (d/name head-bool-type)
:on-change on-change
:name "bool-options"
:options [{:id "bool-opt-union"
:icon i/boolean-union
:label (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")")
:value "union"
:disabled disabled-bool-btns}
{:id "bool-opt-differente"
:icon i/boolean-difference
:label (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")")
:value "difference"
:disabled disabled-bool-btns}
{:id "bool-opt-intersection"
:icon i/boolean-intersection
:label (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")")
:value "intersection"
:disabled disabled-bool-btns}
{:id "bool-opt-exclude"
:icon i/boolean-exclude
:label (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")")
:value "exclude"
:disabled disabled-bool-btns}]}]]
[:div {:class (stl/css :bool-group)}
[:& radio-buttons {:selected (d/name head-bool-type)
:class (stl/css :boolean-radio-btn)
:on-change on-change
:name "bool-options"}
[:& radio-button {:icon i/boolean-union
:value "union"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")")
:id "bool-opt-union"}]
[:& radio-button {:icon i/boolean-difference
:value "difference"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")")
:id "bool-opt-differente"}]
[:& radio-button {:icon i/boolean-intersection
:value "intersection"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")")
:id "bool-opt-intersection"}]
[:& radio-button {:icon i/boolean-exclude
:value "exclude"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")")
:id "bool-opt-exclude"}]]]
[:> icon-button* {:variant "ghost"
:icon i/boolean-flatten
:aria-label (tr "workspace.shape.menu.flatten")
:on-click flatten-objects
:disabled disabled-flatten}]])))
[:button
{:title (tr "workspace.shape.menu.flatten")
:class (stl/css-case
:flatten-button true
:disabled disabled-flatten)
:disabled disabled-flatten
:on-click flatten-objects}
flatten-icon]])))

View File

@@ -4,18 +4,45 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
.boolean-options {
@include sidebar.option-grid-structure;
height: var(--sp-xxxl);
}
.boolean-group {
.bool-group {
display: grid;
grid-template-columns: subgrid;
grid-column: 1 / span 4;
}
.flatten-button {
@extend .button-tertiary;
height: deprecated.$s-32;
width: deprecated.$s-32;
border-radius: deprecated.$br-8;
grid-column: 5 / span 1;
--flatten-icon-foreground-color: var(--icon-foreground);
&.disabled {
cursor: default;
--flatten-icon-foreground-color: var(--button-foreground-color-disabled);
&:hover {
background-color: var(--panel-background-color);
--flatten-icon-foreground-color: var(--button-foreground-color-disabled);
}
}
}
.flatten-icon {
@extend .button-icon;
stroke: var(--flatten-icon-foreground-color);
}
.boolean-radio-btn {
background-color: transparent;
gap: var(--sp-xs);
}

View File

@@ -145,9 +145,9 @@
:on-change on-radius-r3-change
:value (:r3 values)}]]])
[:> icon-button* {:variant "ghost"
[:> icon-button* {:class (stl/css-case :selected radius-expanded)
:variant "ghost"
:on-click toggle-radius-mode
:aria-pressed radius-expanded
:aria-label (if radius-expanded
(tr "workspace.options.radius.hide-all-corners")
(tr "workspace.options.radius.show-single-corners"))

Some files were not shown because too many files have changed in this diff Show More