mirror of
https://github.com/penpot/penpot.git
synced 2026-01-13 08:49:09 -05:00
Compare commits
6 Commits
tokens-api
...
hiru-add-v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
094ad897ab | ||
|
|
b2c45f6e1d | ||
|
|
7a377258d3 | ||
|
|
dd4a18a3ff | ||
|
|
bff005a951 | ||
|
|
776c62051d |
15
CHANGES.md
15
CHANGES.md
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"}
|
||||
|
||||
@@ -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"]))
|
||||
|
||||
@@ -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 _]
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -60,6 +60,9 @@
|
||||
(some? objects)
|
||||
(assoc :objects objects)
|
||||
|
||||
(nil? objects)
|
||||
(dissoc :objects)
|
||||
|
||||
(some? modified-at)
|
||||
(assoc :modified-at modified-at)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"}}}}))))
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
{}))
|
||||
|
||||
|
||||
@@ -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"))))
|
||||
|
||||
@@ -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"}}}}))))
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)]
|
||||
|
||||
|
||||
@@ -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)))
|
||||
@@ -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))
|
||||
|
||||
@@ -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}]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)))
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
107
frontend/src/app/main/ui/components/radio_buttons.cljs
Normal file
107
frontend/src/app/main/ui/components/radio_buttons.cljs
Normal 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]]))
|
||||
79
frontend/src/app/main/ui/components/radio_buttons.scss
Normal file
79
frontend/src/app/main/ui/components/radio_buttons.scss
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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}])))]]]]))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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}]]]]]))
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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*
|
||||
|
||||
@@ -15,7 +15,6 @@
|
||||
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
column-gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
|
||||
@@ -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?}]]))]))
|
||||
@@ -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.
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}]]]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}]]))
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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}]]]])])]]]))
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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*]]
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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")]
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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?
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}]]))
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)}])]))
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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"}]]]))
|
||||
|
||||
|
||||
@@ -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}]]])))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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]])))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user