mirror of
https://github.com/penpot/penpot.git
synced 2025-12-24 06:58:34 -05:00
Compare commits
2 Commits
nitrate-mo
...
tokens-api
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b786b8e07a | ||
|
|
e8ff89836e |
2
.github/workflows/build-tag.yml
vendored
2
.github/workflows/build-tag.yml
vendored
@@ -11,7 +11,7 @@ jobs:
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: ${{ github.ref_name }}
|
||||
build_wasm: "yes"
|
||||
build_wasm: "no"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -20,7 +20,6 @@
|
||||
.rebel_readline_history
|
||||
.repl
|
||||
.shadow-cljs
|
||||
.pnpm-store/
|
||||
/*.jpg
|
||||
/*.md
|
||||
/*.png
|
||||
@@ -72,7 +71,6 @@
|
||||
/library/target/
|
||||
/library/*.zip
|
||||
/external
|
||||
/penpot-nitrate
|
||||
|
||||
clj-profiler/
|
||||
node_modules
|
||||
|
||||
@@ -114,8 +114,6 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
|
||||
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
|
||||
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
|
||||
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
|
||||
- Fix horizontal scroll on layer panel [Taiga #12843](https://tree.taiga.io/project/penpot/issue/12843)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
|
||||
{:id "penpot-design-system"
|
||||
:name "Penpot Design System | Pencil"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
|
||||
{:id "wireframing-kit"
|
||||
:name "Wireframe library"
|
||||
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}
|
||||
|
||||
@@ -36,8 +36,7 @@ export PENPOT_FLAGS="\
|
||||
enable-file-validation \
|
||||
enable-file-schema-validation \
|
||||
enable-redis-cache \
|
||||
enable-subscriptions \
|
||||
enable-nitrate";
|
||||
enable-subscriptions";
|
||||
|
||||
# Default deletion delay for devenv
|
||||
export PENPOT_DELETION_DELAY="24h"
|
||||
@@ -56,8 +55,6 @@ export PENPOT_OBJECTS_STORAGE_BACKEND=s3
|
||||
export PENPOT_OBJECTS_STORAGE_S3_ENDPOINT=http://minio:9000
|
||||
export PENPOT_OBJECTS_STORAGE_S3_BUCKET=penpot
|
||||
|
||||
export PENPOT_NITRATE_BACKEND_URI=http://localhost:3000
|
||||
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
|
||||
@@ -225,8 +225,6 @@
|
||||
[:netty-io-threads {:optional true} ::sm/int]
|
||||
[:executor-threads {:optional true} ::sm/int]
|
||||
|
||||
[:nitrate-backend-uri {:optional true} ::sm/uri]
|
||||
|
||||
;; DEPRECATED
|
||||
[:assets-storage-backend {:optional true} :keyword]
|
||||
[:storage-assets-fs-directory {:optional true} :string]
|
||||
|
||||
@@ -323,7 +323,6 @@
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
::rds/pool (ig/ref ::rds/pool)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
::wrk/executor (ig/ref ::wrk/netty-executor)
|
||||
::session/manager (ig/ref ::session/manager)
|
||||
::ldap/provider (ig/ref ::ldap/provider)
|
||||
@@ -340,9 +339,6 @@
|
||||
::email/blacklist (ig/ref ::email/blacklist)
|
||||
::email/whitelist (ig/ref ::email/whitelist)}
|
||||
|
||||
:app.nitrate/client
|
||||
{::http.client/client (ig/ref ::http.client/client)}
|
||||
|
||||
:app.rpc/management-methods
|
||||
{::http.client/client (ig/ref ::http.client/client)
|
||||
::db/pool (ig/ref ::db/pool)
|
||||
@@ -352,7 +348,6 @@
|
||||
::sto/storage (ig/ref ::sto/storage)
|
||||
::mtx/metrics (ig/ref ::mtx/metrics)
|
||||
::mbus/msgbus (ig/ref ::mbus/msgbus)
|
||||
:app.nitrate/client (ig/ref :app.nitrate/client)
|
||||
::rds/client (ig/ref ::rds/client)
|
||||
::setup/props (ig/ref ::setup/props)}
|
||||
|
||||
|
||||
@@ -1,123 +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.nitrate
|
||||
"Module that make calls to the external nitrate aplication"
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.config :as cf]
|
||||
[app.http.client :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup :as-alias setup]
|
||||
[app.util.json :as json]
|
||||
[clojure.core :as c]
|
||||
[integrant.core :as ig]))
|
||||
|
||||
|
||||
(def baseuri (cf/get :nitrate-backend-uri))
|
||||
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (sm/decoder schema sm/json-transformer)
|
||||
check-fn (sm/check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
|
||||
(defn- request-builder
|
||||
[cfg method uri management-key profile-id]
|
||||
(fn []
|
||||
(http/req! cfg {:method method
|
||||
:headers {"content-type" "application/json"
|
||||
"accept" "application/json"
|
||||
"x-shared-key" management-key
|
||||
"x-profile-id" (str profile-id)}
|
||||
:uri uri
|
||||
:version :http1.1})))
|
||||
|
||||
|
||||
(defn- with-retries
|
||||
[handler max-retries]
|
||||
(fn []
|
||||
(loop [attempt 1]
|
||||
(let [result (try
|
||||
(handler)
|
||||
(catch Exception e
|
||||
(if (< attempt max-retries)
|
||||
::retry
|
||||
(do
|
||||
;; TODO Error handling
|
||||
(l/error :hint "request fail after multiple retries" :cause e)
|
||||
nil))))]
|
||||
(if (= result ::retry)
|
||||
(recur (inc attempt))
|
||||
result)))))
|
||||
|
||||
|
||||
(defn- with-validate [handler uri schema]
|
||||
(fn []
|
||||
(let [coercer-http (coercer schema
|
||||
:type :validation
|
||||
:hint (str "invalid data received calling " uri))]
|
||||
(try
|
||||
(coercer-http (-> (handler) :body json/decode))
|
||||
(catch Exception e
|
||||
;; TODO Error handling
|
||||
(l/error :hint "error validating json response" :cause e)
|
||||
nil)))))
|
||||
|
||||
(defn- request-to-nitrate
|
||||
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
|
||||
(with-retries 3)
|
||||
(with-validate uri schema))]
|
||||
(full-http-call)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; API
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn call
|
||||
[cfg method params]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [client (get cfg ::client)
|
||||
method (get client method)]
|
||||
(method params))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def ^:private schema:organization
|
||||
[:map
|
||||
[:id ::sm/text]
|
||||
[:name ::sm/text]])
|
||||
|
||||
|
||||
(defn- get-team-org
|
||||
[cfg {:keys [team-id] :as params}]
|
||||
(request-to-nitrate cfg :get (str baseuri "/api/teams/" (str team-id)) schema:organization params))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; INITIALIZATION
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defmethod ig/init-key ::client
|
||||
[_ {:keys [::setup/props] :as cfg}]
|
||||
(if (contains? cf/flags :nitrate)
|
||||
(let [management-key (or (cf/get :management-api-key)
|
||||
(get props :management-key))
|
||||
cfg (assoc cfg ::management-key management-key)]
|
||||
{:get-team-org (partial get-team-org cfg)})
|
||||
{}))
|
||||
|
||||
(defmethod ig/halt-key! ::client
|
||||
[_ {:keys []}]
|
||||
(do :stuff))
|
||||
@@ -296,7 +296,6 @@
|
||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
|
||||
(->> (sv/scan-ns
|
||||
'app.rpc.management.subscription
|
||||
'app.rpc.management.nitrate
|
||||
'app.rpc.management.exporter)
|
||||
(map (partial process-method cfg "management" wrap-management))
|
||||
(into {}))))
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
[app.main :as-alias main]
|
||||
[app.media :as media]
|
||||
[app.msgbus :as mbus]
|
||||
[app.nitrate :as nitrate]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as-alias doc]
|
||||
@@ -173,12 +172,6 @@
|
||||
(map decode-row)
|
||||
(map process-permissions)))
|
||||
|
||||
(defn- add-org-to-team
|
||||
[cfg team params]
|
||||
(let [params (assoc (or params {}) :team-id (:id team))
|
||||
org (nitrate/call cfg :get-team-org params)]
|
||||
(assoc team :organization-id (:id org) :organization-name (:name org))))
|
||||
|
||||
(defn get-teams
|
||||
[conn profile-id]
|
||||
(let [profile (profile/get-profile conn profile-id)
|
||||
@@ -197,9 +190,7 @@
|
||||
::sm/params schema:get-teams}
|
||||
[{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}]
|
||||
(dm/with-open [conn (db/open pool)]
|
||||
(cond->> (get-teams conn profile-id)
|
||||
(contains? cf/flags :nitrate)
|
||||
(map #(add-org-to-team cfg % params)))))
|
||||
(get-teams conn profile-id)))
|
||||
|
||||
(def ^:private sql:get-owned-teams
|
||||
"SELECT t.id, t.name,
|
||||
|
||||
@@ -1,112 +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.rpc.management.nitrate
|
||||
"Internal Nitrate HTTP API.
|
||||
Provides authenticated access to organization management and token validation endpoints.
|
||||
All requests must include a valid shared key token in the `x-shared-key` header, and
|
||||
a cookie `auth-token` with the user token.
|
||||
They will return `401 Unauthorized` if the shared key or user token are invalid."
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.db :as db]
|
||||
[app.msgbus :as mbus]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.rpc.commands.profile :as profile]
|
||||
[app.rpc.doc :as doc]
|
||||
[app.util.services :as sv]))
|
||||
|
||||
;; ---- API: authenticate
|
||||
(def ^:private schema:profile
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:email :string]
|
||||
[:photo-url :string]])
|
||||
|
||||
(sv/defmethod ::authenticate
|
||||
"Authenticate an user
|
||||
@api GET /authenticate
|
||||
@returns
|
||||
200 OK: Returns the authenticated user."
|
||||
{::doc/added "2.12"
|
||||
::sm/result schema:profile}
|
||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||
(let [profile (profile/get-profile cfg profile-id)]
|
||||
{:id (get profile :id)
|
||||
:name (get profile :fullname)
|
||||
:email (get profile :email)
|
||||
:photo-url (files/resolve-public-uri (get profile :photo-id))}))
|
||||
|
||||
;; ---- API: get-teams
|
||||
|
||||
(def ^:private sql:get-teams
|
||||
"SELECT t.*
|
||||
FROM team AS t
|
||||
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
||||
WHERE tpr.profile_id = ?
|
||||
AND tpr.is_owner = 't'
|
||||
AND t.is_default = 'f'
|
||||
AND t.deleted_at is null;")
|
||||
|
||||
(def ^:private schema:team
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]])
|
||||
|
||||
(def ^:private schema:get-teams-result
|
||||
[:vector schema:team])
|
||||
|
||||
(sv/defmethod ::get-teams
|
||||
"List teams for which current user is owner.
|
||||
@api GET /get-teams
|
||||
@returns
|
||||
200 OK: Returns the list of teams for the user."
|
||||
{::doc/added "2.12"
|
||||
::sm/result schema:get-teams-result}
|
||||
[cfg {:keys [::rpc/profile-id]}]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
||||
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
||||
(map #(select-keys % [:id :name]))))))
|
||||
|
||||
;; ---- API: notify-team-change
|
||||
|
||||
(def ^:private schema:notify-team-change
|
||||
[:map
|
||||
[:id ::sm/uuid]
|
||||
[:organization-id ::sm/text]])
|
||||
|
||||
|
||||
(sv/defmethod ::notify-team-change
|
||||
"Notify to Penpot a team change from nitrate
|
||||
@api POST /notify-team-change
|
||||
@returns
|
||||
200 OK"
|
||||
{::doc/added "2.12"
|
||||
::sm/params schema:notify-team-change
|
||||
::rpc/auth false}
|
||||
[cfg {:keys [id organization-id organization-name]}]
|
||||
(when (contains? cf/flags :nitrate)
|
||||
(let [msgbus (::mbus/msgbus cfg)]
|
||||
(mbus/pub! msgbus
|
||||
;;TODO There is a bug on dashboard with teams notifications.
|
||||
;;For now we send it to uuid/zero instead of team-id
|
||||
:topic uuid/zero
|
||||
:message {:type :team-org-change
|
||||
:team-id id
|
||||
:organization-id organization-id
|
||||
:organization-name organization-name}))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[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]
|
||||
@@ -378,7 +379,7 @@
|
||||
[:type [:= :set-token]]
|
||||
[:set-id ::sm/uuid]
|
||||
[:token-id ::sm/uuid]
|
||||
[:attrs [:maybe ctob/schema:token-attrs]]]]
|
||||
[:attrs [:maybe cto/schema:token-attrs]]]]
|
||||
|
||||
[:set-token-set
|
||||
[:map {:title "SetTokenSetChange"}
|
||||
|
||||
@@ -82,113 +82,6 @@
|
||||
(declare create-svg-children)
|
||||
(declare parse-svg-element)
|
||||
|
||||
(defn- process-gradient-stops
|
||||
"Processes gradient stops to extract stop-color and stop-opacity from style attributes
|
||||
and convert them to direct attributes. This ensures stops with style='stop-color:#...;stop-opacity:1'
|
||||
are properly converted to stop-color and stop-opacity attributes."
|
||||
[stops]
|
||||
(mapv (fn [stop]
|
||||
(let [stop-attrs (:attrs stop)
|
||||
stop-style (get stop-attrs :style)
|
||||
;; Parse style if it's a string using csvg/parse-style utility
|
||||
parsed-style (when (and (string? stop-style) (seq stop-style))
|
||||
(csvg/parse-style stop-style))
|
||||
;; Extract stop-color and stop-opacity from style
|
||||
style-stop-color (when parsed-style (:stop-color parsed-style))
|
||||
style-stop-opacity (when parsed-style (:stop-opacity parsed-style))
|
||||
;; Merge: use direct attributes first, then style values as fallback
|
||||
final-attrs (cond-> stop-attrs
|
||||
(and style-stop-color (not (contains? stop-attrs :stop-color)))
|
||||
(assoc :stop-color style-stop-color)
|
||||
|
||||
(and style-stop-opacity (not (contains? stop-attrs :stop-opacity)))
|
||||
(assoc :stop-opacity style-stop-opacity)
|
||||
|
||||
;; Remove style attribute if we've extracted its values
|
||||
(or style-stop-color style-stop-opacity)
|
||||
(dissoc :style))]
|
||||
(assoc stop :attrs final-attrs)))
|
||||
stops))
|
||||
|
||||
(defn- resolve-gradient-href
|
||||
"Resolves xlink:href references in gradients by merging the referenced gradient's
|
||||
stops and attributes with the referencing gradient. This ensures gradients that
|
||||
reference other gradients (like linearGradient3550 referencing linearGradient3536)
|
||||
inherit the stops from the base gradient.
|
||||
|
||||
According to SVG spec, when a gradient has xlink:href:
|
||||
- It inherits all attributes from the referenced gradient
|
||||
- It inherits all stops from the referenced gradient
|
||||
- The referencing gradient's attributes override the base ones
|
||||
- If the referencing gradient has stops, they replace the base stops
|
||||
|
||||
Returns the defs map with all gradient href references resolved."
|
||||
[defs]
|
||||
(letfn [(resolve-gradient [gradient-id gradient-node defs visited]
|
||||
(if (contains? visited gradient-id)
|
||||
(do
|
||||
#?(:cljs (js/console.warn "[resolve-gradient] Circular reference detected for" gradient-id)
|
||||
:clj nil)
|
||||
gradient-node) ;; Avoid circular references
|
||||
(let [attrs (:attrs gradient-node)
|
||||
href-id (or (:href attrs) (:xlink:href attrs))
|
||||
href-id (when (and (string? href-id) (pos? (count href-id)))
|
||||
(subs href-id 1)) ;; Remove leading #
|
||||
|
||||
base-gradient (when (and href-id (contains? defs href-id))
|
||||
(get defs href-id))
|
||||
|
||||
resolved-base (when base-gradient (resolve-gradient href-id base-gradient defs (conj visited gradient-id)))]
|
||||
|
||||
(if resolved-base
|
||||
;; Merge: base gradient attributes + referencing gradient attributes
|
||||
;; Use referencing gradient's stops if present, otherwise use base stops
|
||||
(let [base-attrs (:attrs resolved-base)
|
||||
ref-attrs (:attrs gradient-node)
|
||||
|
||||
;; Start with base attributes (without id), then merge with ref attributes
|
||||
;; This ensures ref attributes override base ones
|
||||
base-attrs-clean (dissoc base-attrs :id)
|
||||
ref-attrs-clean (dissoc ref-attrs :href :xlink:href :id)
|
||||
|
||||
;; Special handling for gradientTransform: if both have it, combine them
|
||||
base-transform (get base-attrs :gradientTransform)
|
||||
ref-transform (get ref-attrs :gradientTransform)
|
||||
combined-transform (cond
|
||||
(and base-transform ref-transform)
|
||||
(str base-transform " " ref-transform) ;; Apply base first, then ref
|
||||
:else (or ref-transform base-transform))
|
||||
|
||||
;; Merge attributes: base first, then ref (ref overrides)
|
||||
merged-attrs (-> (d/deep-merge base-attrs-clean ref-attrs-clean)
|
||||
(cond-> combined-transform
|
||||
(assoc :gradientTransform combined-transform)))
|
||||
|
||||
;; If referencing gradient has content (stops), use it; otherwise use base content
|
||||
final-content (if (seq (:content gradient-node))
|
||||
(:content gradient-node)
|
||||
(:content resolved-base))
|
||||
|
||||
;; Process stops to extract stop-color and stop-opacity from style attributes
|
||||
processed-content (process-gradient-stops final-content)
|
||||
|
||||
result {:tag (:tag gradient-node)
|
||||
:attrs (assoc merged-attrs :id gradient-id)
|
||||
:content processed-content}]
|
||||
result)
|
||||
;; Process stops even for gradients without references to extract style attributes
|
||||
(let [processed-content (process-gradient-stops (:content gradient-node))]
|
||||
(assoc gradient-node :content processed-content))))))]
|
||||
(let [gradient-tags #{:linearGradient :radialGradient}
|
||||
result (reduce-kv
|
||||
(fn [acc id node]
|
||||
(if (contains? gradient-tags (:tag node))
|
||||
(assoc acc id (resolve-gradient id node defs #{}))
|
||||
(assoc acc id node)))
|
||||
{}
|
||||
defs)]
|
||||
result)))
|
||||
|
||||
(defn create-svg-shapes
|
||||
([svg-data pos objects frame-id parent-id selected center?]
|
||||
(create-svg-shapes (uuid/next) svg-data pos objects frame-id parent-id selected center?))
|
||||
@@ -219,9 +112,6 @@
|
||||
(csvg/fix-percents)
|
||||
(csvg/extract-defs))
|
||||
|
||||
;; Resolve gradient href references in all defs before processing shapes
|
||||
def-nodes (resolve-gradient-href def-nodes)
|
||||
|
||||
;; In penpot groups have the size of their children. To
|
||||
;; respect the imported svg size and empty space let's create
|
||||
;; a transparent shape as background to respect the imported
|
||||
@@ -252,23 +142,12 @@
|
||||
(reduce (partial create-svg-children objects selected frame-id root-id svg-data)
|
||||
[unames []]
|
||||
(d/enumerate (->> (:content svg-data)
|
||||
(mapv #(csvg/inherit-attributes root-attrs %)))))
|
||||
(mapv #(csvg/inherit-attributes root-attrs %)))))]
|
||||
|
||||
;; Collect all defs from children and merge into root shape
|
||||
all-defs-from-children (reduce (fn [acc child]
|
||||
(if-let [child-defs (:svg-defs child)]
|
||||
(merge acc child-defs)
|
||||
acc))
|
||||
{}
|
||||
children)
|
||||
|
||||
;; Merge defs from svg-data and children into root shape
|
||||
root-shape-with-defs (assoc root-shape :svg-defs (merge def-nodes all-defs-from-children))]
|
||||
|
||||
[root-shape-with-defs children])))
|
||||
[root-shape children])))
|
||||
|
||||
(defn create-raw-svg
|
||||
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs] :as data}]
|
||||
[name frame-id {:keys [x y width height offset-x offset-y]} {:keys [attrs] :as data}]
|
||||
(let [props (csvg/attrs->props attrs)
|
||||
vbox (grc/make-rect offset-x offset-y width height)]
|
||||
(cts/setup-shape
|
||||
@@ -281,11 +160,10 @@
|
||||
:y y
|
||||
:content data
|
||||
:svg-attrs props
|
||||
:svg-viewbox vbox
|
||||
:svg-defs defs})))
|
||||
:svg-viewbox vbox})))
|
||||
|
||||
(defn create-svg-root
|
||||
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs defs] :as svg-data}]
|
||||
[id frame-id parent-id {:keys [name x y width height offset-x offset-y attrs]}]
|
||||
(let [props (-> (dissoc attrs :viewBox :view-box :xmlns)
|
||||
(d/without-keys csvg/inheritable-props)
|
||||
(csvg/attrs->props))]
|
||||
@@ -299,8 +177,7 @@
|
||||
:height height
|
||||
:x (+ x offset-x)
|
||||
:y (+ y offset-y)
|
||||
:svg-attrs props
|
||||
:svg-defs defs})))
|
||||
:svg-attrs props})))
|
||||
|
||||
(defn create-svg-children
|
||||
[objects selected frame-id parent-id svg-data [unames children] [_index svg-element]]
|
||||
@@ -321,7 +198,7 @@
|
||||
|
||||
|
||||
(defn create-group
|
||||
[name frame-id {:keys [x y width height offset-x offset-y defs] :as svg-data} {:keys [attrs]}]
|
||||
[name frame-id {:keys [x y width height offset-x offset-y] :as svg-data} {:keys [attrs]}]
|
||||
(let [transform (csvg/parse-transform (:transform attrs))
|
||||
attrs (-> attrs
|
||||
(d/without-keys csvg/inheritable-props)
|
||||
@@ -337,8 +214,7 @@
|
||||
:height height
|
||||
:svg-transform transform
|
||||
:svg-attrs attrs
|
||||
:svg-viewbox vbox
|
||||
:svg-defs defs})))
|
||||
:svg-viewbox vbox})))
|
||||
|
||||
(defn create-path-shape [name frame-id svg-data {:keys [attrs] :as data}]
|
||||
(when (and (contains? attrs :d) (seq (:d attrs)))
|
||||
@@ -647,21 +523,6 @@
|
||||
:else (dm/str tag))]
|
||||
(dm/str "svg-" suffix)))
|
||||
|
||||
(defn- filter-valid-def-references
|
||||
"Filters out false positive references that are not valid def IDs.
|
||||
Filters out:
|
||||
- Colors in style attributes (hex colors like #f9dd67)
|
||||
- Style fragments that contain CSS keywords (like stop-opacity)
|
||||
- References that don't exist in defs"
|
||||
[ref-ids defs]
|
||||
(let [is-style-fragment? (fn [ref-id]
|
||||
(or (clr/hex-color-string? (str "#" ref-id))
|
||||
(str/includes? ref-id ";") ;; Contains CSS separator
|
||||
(str/includes? ref-id "stop-opacity") ;; CSS keyword
|
||||
(str/includes? ref-id "stop-color")))] ;; CSS keyword
|
||||
(->> ref-ids
|
||||
(remove is-style-fragment?) ;; Filter style fragments and hex colors
|
||||
(filter #(contains? defs %))))) ;; Only existing defs
|
||||
|
||||
(defn parse-svg-element
|
||||
[frame-id svg-data {:keys [tag attrs hidden] :as element} unames]
|
||||
@@ -673,11 +534,7 @@
|
||||
(let [name (or (:id attrs) (tag->name tag))
|
||||
att-refs (csvg/find-attr-references attrs)
|
||||
defs (get svg-data :defs)
|
||||
valid-refs (filter-valid-def-references att-refs defs)
|
||||
all-refs (csvg/find-def-references defs valid-refs)
|
||||
;; Filter the final result to ensure all references are valid defs
|
||||
;; This prevents false positives from style attributes in gradient stops
|
||||
references (filter-valid-def-references all-refs defs)
|
||||
references (csvg/find-def-references defs att-refs)
|
||||
|
||||
href-id (or (:href attrs) (:xlink:href attrs) " ")
|
||||
href-id (if (and (string? href-id)
|
||||
|
||||
@@ -8,9 +8,112 @@
|
||||
(: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."
|
||||
@@ -80,56 +183,6 @@
|
||||
(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))
|
||||
|
||||
|
||||
@@ -145,10 +145,7 @@
|
||||
|
||||
;; A temporal flag, enables backend code use more extensivelly
|
||||
;; redis for caching data
|
||||
:redis-cache
|
||||
|
||||
;; Activates the nitrate module
|
||||
:nitrate})
|
||||
:redis-cache})
|
||||
|
||||
(def all-flags
|
||||
(set/union email login varia))
|
||||
|
||||
15
common/src/app/common/i18n.cljc
Normal file
15
common/src/app/common/i18n.cljc
Normal file
@@ -0,0 +1,15 @@
|
||||
;; 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)
|
||||
@@ -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-token-id % token-attrs))
|
||||
(pcb/update-shapes changes [shape-id] #(cto/unapply-tokens-from-shape % token-attrs))
|
||||
changes)))
|
||||
|
||||
check-shape
|
||||
|
||||
@@ -132,94 +132,3 @@ Some naming conventions:
|
||||
(if-let [last-period (str/last-index-of s ".")]
|
||||
[(subs s 0 (inc last-period)) (subs s (inc last-period))]
|
||||
[s ""]))
|
||||
|
||||
;; Tree building functions --------------------------------------------------
|
||||
|
||||
"Build tree structure from flat list of paths"
|
||||
|
||||
"`build-tree-root` is the main function to build the tree."
|
||||
|
||||
"Receives a list of segments with 'name' properties representing paths,
|
||||
and a separator string."
|
||||
"E.g segments = [{... :name 'one/two/three'} {... :name 'one/two/four'} {... :name 'one/five'}]"
|
||||
|
||||
"Transforms into a tree structure like:
|
||||
[{:name 'one'
|
||||
:path 'one'
|
||||
:depth 0
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{:name 'two'
|
||||
:path 'one.two'
|
||||
:depth 1
|
||||
:leaf nil
|
||||
:children-fn (fn [] [{... :name 'three'} {... :name 'four'}])}
|
||||
{:name 'five'
|
||||
:path 'one.five'
|
||||
:depth 1
|
||||
:leaf {... :name 'five'}
|
||||
...}])}]"
|
||||
|
||||
(defn- sort-by-children
|
||||
"Sorts segments so that those with children come first."
|
||||
[segments separator]
|
||||
(sort-by (fn [segment]
|
||||
(let [path (split-path (:name segment) :separator separator)
|
||||
path-length (count path)]
|
||||
(if (= path-length 1)
|
||||
1
|
||||
0)))
|
||||
segments))
|
||||
|
||||
(defn- group-by-first-segment
|
||||
"Groups segments by their first path segment and update segment name."
|
||||
[segments separator]
|
||||
(reduce (fn [acc segment]
|
||||
(let [[first-segment & remaining-segments] (split-path (:name segment) :separator separator)
|
||||
rest-path (when (seq remaining-segments) (join-path remaining-segments :separator separator :with-spaces? false))]
|
||||
(update acc first-segment (fnil conj [])
|
||||
(if rest-path
|
||||
(assoc segment :name rest-path)
|
||||
segment))))
|
||||
{}
|
||||
segments))
|
||||
|
||||
(defn- sort-and-group-segments
|
||||
"Sorts elements and groups them by their first path segment."
|
||||
[segments separator]
|
||||
(let [sorted (sort-by-children segments separator)
|
||||
grouped (group-by-first-segment sorted separator)]
|
||||
grouped))
|
||||
|
||||
(defn- build-tree-node
|
||||
"Builds a single tree node with lazy children."
|
||||
[segment-name remaining-segments separator parent-path depth]
|
||||
(let [current-path (if parent-path
|
||||
(str parent-path "." segment-name)
|
||||
segment-name)
|
||||
|
||||
is-leaf? (and (seq remaining-segments)
|
||||
(every? (fn [segment]
|
||||
(let [remaining-segment-name (first (split-path (:name segment) :separator separator))]
|
||||
(= segment-name remaining-segment-name)))
|
||||
remaining-segments))
|
||||
|
||||
leaf-segment (when is-leaf? (first remaining-segments))
|
||||
node {:name segment-name
|
||||
:path current-path
|
||||
:depth depth
|
||||
:leaf leaf-segment
|
||||
:children-fn (when-not is-leaf?
|
||||
(fn []
|
||||
(let [grouped-elements (sort-and-group-segments remaining-segments separator)]
|
||||
(mapv (fn [[child-segment-name remaining-child-segments]]
|
||||
(build-tree-node child-segment-name remaining-child-segments separator current-path (inc depth)))
|
||||
grouped-elements))))}]
|
||||
node))
|
||||
|
||||
(defn build-tree-root
|
||||
"Builds the root level of the tree."
|
||||
[segments separator]
|
||||
(let [grouped-elements (sort-and-group-segments segments separator)]
|
||||
(mapv (fn [[segment-name remaining-segments]]
|
||||
(build-tree-node segment-name remaining-segments separator nil 0))
|
||||
grouped-elements)))
|
||||
|
||||
@@ -92,6 +92,16 @@
|
||||
[& 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))
|
||||
@@ -270,6 +280,13 @@
|
||||
(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)
|
||||
|
||||
@@ -546,19 +546,9 @@
|
||||
filter-values)))
|
||||
|
||||
(defn extract-ids [val]
|
||||
;; Extract referenced ids from string values like "url(#myId)".
|
||||
;; Non-string values (maps, numbers, nil, etc.) return an empty seq
|
||||
;; to avoid re-seq type errors when attributes carry nested structures.
|
||||
(cond
|
||||
(string? val)
|
||||
(when (some? val)
|
||||
(->> (re-seq xml-id-regex val)
|
||||
(mapv second))
|
||||
|
||||
(sequential? val)
|
||||
(mapcat extract-ids val)
|
||||
|
||||
:else
|
||||
[]))
|
||||
(mapv second))))
|
||||
|
||||
(defn fix-dot-number
|
||||
"Fixes decimal numbers starting in dot but without leading 0"
|
||||
|
||||
@@ -234,15 +234,16 @@
|
||||
"Calculate the boolean content from shape and objects. Returns a
|
||||
packed PathData instance"
|
||||
[shape objects]
|
||||
(let [content (calc-bool-content* shape objects)]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content (get shape :bool-type)
|
||||
(get shape :shapes))
|
||||
(calc-bool-content* shape objects))]
|
||||
(impl/path-data content)))
|
||||
|
||||
(defn update-bool-shape
|
||||
"Calculates the selrect+points for the boolean shape"
|
||||
[shape objects]
|
||||
(let [content (if (fn? wasm:calc-bool-content)
|
||||
(wasm:calc-bool-content shape objects)
|
||||
(calc-bool-content shape objects))
|
||||
(let [content (calc-bool-content shape objects)
|
||||
shape (assoc shape :content content)]
|
||||
(update-geometry shape)))
|
||||
|
||||
|
||||
@@ -9,13 +9,13 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.schema.generators :as sg]
|
||||
[clojure.data :as data]
|
||||
[app.common.time :as ct]
|
||||
[clojure.set :as set]
|
||||
[cuerdas.core :as str]
|
||||
[malli.util :as mu]))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; HELPERS
|
||||
;; GENERAL HELPERS
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn- schema-keys
|
||||
@@ -45,32 +45,33 @@
|
||||
[token-name token-value]
|
||||
(let [token-references (find-token-value-references token-value)
|
||||
self-reference? (get token-references token-name)]
|
||||
self-reference?))
|
||||
(boolean self-reference?)))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SCHEMA
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
(defn composite-token-reference?
|
||||
"Predicate if a composite token is a reference value - a string pointing to another token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
(def token-type->dtcg-token-type
|
||||
{:boolean "boolean"
|
||||
:border-radius "borderRadius"
|
||||
:shadow "shadow"
|
||||
:color "color"
|
||||
:dimensions "dimension"
|
||||
:font-family "fontFamilies"
|
||||
:font-size "fontSizes"
|
||||
:font-weight "fontWeights"
|
||||
:letter-spacing "letterSpacing"
|
||||
:number "number"
|
||||
:opacity "opacity"
|
||||
:other "other"
|
||||
:rotation "rotation"
|
||||
:shadow "shadow"
|
||||
:sizing "sizing"
|
||||
:spacing "spacing"
|
||||
:string "string"
|
||||
:stroke-width "borderWidth"
|
||||
:text-case "textCase"
|
||||
:text-decoration "textDecoration"
|
||||
:font-weight "fontWeights"
|
||||
:typography "typography"})
|
||||
|
||||
(def dtcg-token-type->token-type
|
||||
@@ -82,14 +83,13 @@
|
||||
"boxShadow" :shadow)))
|
||||
|
||||
(def composite-token-type->dtcg-token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"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."
|
||||
(assoc token-type->dtcg-token-type
|
||||
:line-height "lineHeights"))
|
||||
|
||||
(def composite-dtcg-token-type->token-type
|
||||
"Custom set of conversion keys for composite typography token with `:line-height` available.
|
||||
(Penpot doesn't support `:line-height` token)"
|
||||
"Same as above, in the opposite direction."
|
||||
(assoc dtcg-token-type->token-type
|
||||
"lineHeights" :line-height
|
||||
"lineHeight" :line-height))
|
||||
@@ -97,93 +97,95 @@
|
||||
(def token-types
|
||||
(into #{} (keys token-type->dtcg-token-type)))
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; 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}
|
||||
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:fill {:optional true} token-name-ref]
|
||||
[:stroke-color {:optional true} token-name-ref]])
|
||||
(def schema:token-type
|
||||
[::sm/one-of {:decode/json (fn [type]
|
||||
(if (string? type)
|
||||
(dtcg-token-type->token-type type)
|
||||
type))}
|
||||
|
||||
(def color-keys (schema-keys schema:color))
|
||||
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} token-name-ref]
|
||||
[:r2 {:optional true} token-name-ref]
|
||||
[:r3 {:optional true} token-name-ref]
|
||||
[:r4 {:optional true} token-name-ref]])
|
||||
[: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:shadow
|
||||
[:map {:title "ShadowTokenAttrs"}
|
||||
[:shadow {:optional true} token-name-ref]])
|
||||
|
||||
(def shadow-keys (schema-keys schema:shadow))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
[:stroke-width {:optional true} token-name-ref]])
|
||||
[:fill {:optional true} schema:token-name]
|
||||
[:stroke-color {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
(def color-keys (schema-keys schema:color))
|
||||
|
||||
(def ^:private schema:sizing-base
|
||||
[:map {:title "SizingBaseTokenAttrs"}
|
||||
[:width {:optional true} token-name-ref]
|
||||
[:height {:optional true} token-name-ref]])
|
||||
[:width {:optional true} schema:token-name]
|
||||
[:height {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:sizing-layout-item
|
||||
[:map {:title "SizingLayoutItemTokenAttrs"}
|
||||
[: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]])
|
||||
[: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))
|
||||
|
||||
(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} token-name-ref]
|
||||
[:column-gap {:optional true} token-name-ref]])
|
||||
[:row-gap {:optional true} schema:token-name]
|
||||
[:column-gap {:optional true} schema:token-name]])
|
||||
|
||||
(def ^:private schema:spacing-padding
|
||||
[:map {:title "SpacingPaddingTokenAttrs"}
|
||||
[: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} 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
|
||||
schema:spacing-padding
|
||||
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))
|
||||
[: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
|
||||
@@ -192,6 +194,29 @@
|
||||
|
||||
(def spacing-gap-padding-keys (schema-keys schema:spacing-gap-padding))
|
||||
|
||||
(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))
|
||||
|
||||
(def ^:private schema:spacing
|
||||
(-> (reduce mu/union [schema:spacing-gap
|
||||
schema:spacing-padding
|
||||
schema:spacing-margin])
|
||||
(mu/update-properties assoc :title "SpacingTokenAttrs")))
|
||||
|
||||
(def spacing-keys (schema-keys schema:spacing))
|
||||
|
||||
(def ^:private schema:stroke-width
|
||||
[:map
|
||||
[:stroke-width {:optional true} schema:token-name]])
|
||||
|
||||
(def stroke-width-keys (schema-keys schema:stroke-width))
|
||||
|
||||
(def ^:private schema:dimensions
|
||||
(-> (reduce mu/union [schema:sizing
|
||||
schema:spacing
|
||||
@@ -201,91 +226,109 @@
|
||||
|
||||
(def dimensions-keys (schema-keys schema:dimensions))
|
||||
|
||||
(def ^:private schema:axis
|
||||
[:map
|
||||
[:x {:optional true} token-name-ref]
|
||||
[:y {:optional true} token-name-ref]])
|
||||
|
||||
(def axis-keys (schema-keys schema:axis))
|
||||
|
||||
(def ^:private schema:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[: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]])
|
||||
[:font-family {:optional true} schema:token-name]])
|
||||
|
||||
(def font-family-keys (schema-keys schema:font-family))
|
||||
|
||||
(def ^:private schema:text-case
|
||||
[:map
|
||||
[:text-case {:optional true} token-name-ref]])
|
||||
(def ^:private schema:font-size
|
||||
[:map {:title "FontSizeTokenAttrs"}
|
||||
[:font-size {:optional true} schema:token-name]])
|
||||
|
||||
(def text-case-keys (schema-keys schema:text-case))
|
||||
(def font-size-keys (schema-keys schema:font-size))
|
||||
|
||||
(def ^:private schema:font-weight
|
||||
[:map
|
||||
[:font-weight {:optional true} token-name-ref]])
|
||||
[:font-weight {:optional true} schema:token-name]])
|
||||
|
||||
(def font-weight-keys (schema-keys schema:font-weight))
|
||||
|
||||
(def ^:private schema:typography
|
||||
[:map
|
||||
[:typography {:optional true} token-name-ref]])
|
||||
(def ^:private schema:letter-spacing
|
||||
[:map {:title "LetterSpacingTokenAttrs"}
|
||||
[:letter-spacing {:optional true} schema:token-name]])
|
||||
|
||||
(def typography-token-keys (schema-keys schema:typography))
|
||||
(def letter-spacing-keys (schema-keys schema:letter-spacing))
|
||||
|
||||
(def ^:private schema:text-decoration
|
||||
[:map
|
||||
[:text-decoration {:optional true} token-name-ref]])
|
||||
(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 text-decoration-keys (schema-keys schema:text-decoration))
|
||||
(def line-height-keys (schema-keys schema:line-height))
|
||||
|
||||
(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:rotation
|
||||
[:map {:title "RotationTokenAttrs"}
|
||||
[:rotation {:optional true} schema:token-name]])
|
||||
|
||||
(def rotation-keys (schema-keys schema:rotation))
|
||||
|
||||
(def ^:private schema:number
|
||||
(-> (reduce mu/union [[:map [:line-height {:optional true} token-name-ref]]
|
||||
(-> (reduce mu/union [schema:line-height
|
||||
schema:rotation])
|
||||
(mu/update-properties assoc :title "NumberTokenAttrs")))
|
||||
|
||||
(def number-keys (schema-keys schema:number))
|
||||
|
||||
(def all-keys (set/union color-keys
|
||||
(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
|
||||
border-radius-keys
|
||||
shadow-keys
|
||||
stroke-width-keys
|
||||
sizing-keys
|
||||
opacity-keys
|
||||
spacing-keys
|
||||
color-keys
|
||||
dimensions-keys
|
||||
axis-keys
|
||||
number-keys
|
||||
opacity-keys
|
||||
rotation-keys
|
||||
shadow-keys
|
||||
sizing-keys
|
||||
spacing-keys
|
||||
stroke-width-keys
|
||||
typography-keys
|
||||
typography-token-keys
|
||||
number-keys))
|
||||
typography-token-keys))
|
||||
|
||||
(def ^:private schema:tokens
|
||||
[:map {:title "GenericTokenAttrs"}])
|
||||
@@ -306,11 +349,28 @@
|
||||
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
|
||||
@@ -352,21 +412,13 @@
|
||||
(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))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TOKEN SHAPE ATTRIBUTES
|
||||
;; HELPERS for token attributes by shape type
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(def position-attributes #{:x :y})
|
||||
(def ^:private position-attributes #{:x :y})
|
||||
|
||||
(def generic-attributes
|
||||
(def ^:private generic-attributes
|
||||
(set/union color-keys
|
||||
stroke-width-keys
|
||||
rotation-keys
|
||||
@@ -375,20 +427,22 @@
|
||||
shadow-keys
|
||||
position-attributes))
|
||||
|
||||
(def rect-attributes
|
||||
(def ^:private rect-attributes
|
||||
(set/union generic-attributes
|
||||
border-radius-keys))
|
||||
|
||||
(def frame-with-layout-attributes
|
||||
(def ^:private frame-with-layout-attributes
|
||||
(set/union rect-attributes
|
||||
spacing-gap-padding-keys))
|
||||
|
||||
(def text-attributes
|
||||
(def ^:private 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
|
||||
@@ -404,12 +458,14 @@
|
||||
nil))
|
||||
|
||||
(defn appliable-attrs-for-shape
|
||||
"Returns intersection of shape `attributes` for `shape-type`."
|
||||
"Returns which ones of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes shape-type is-layout]
|
||||
(set/intersection attributes (shape-type->attributes shape-type is-layout)))
|
||||
|
||||
(defn any-appliable-attr-for-shape?
|
||||
"Checks if `token-type` supports given shape `attributes`."
|
||||
"Returns if any of the given `attributes` can be applied to a shape
|
||||
of type `shape-type` and `is-layout`."
|
||||
[attributes token-type is-layout]
|
||||
(d/not-empty? (appliable-attrs-for-shape attributes token-type is-layout)))
|
||||
|
||||
@@ -420,42 +476,6 @@
|
||||
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}
|
||||
@@ -481,7 +501,48 @@
|
||||
:stroke-color #{:color}})
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; TYPOGRAPHY
|
||||
;; 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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn split-font-family
|
||||
@@ -544,17 +605,3 @@
|
||||
(when (font-weight-values weight)
|
||||
(cond-> {:weight weight}
|
||||
italic? (assoc :style "italic")))))
|
||||
|
||||
(defn typography-composite-token-reference?
|
||||
"Predicate if a typography composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; SHADOW
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn shadow-composite-token-reference?
|
||||
"Predicate if a shadow composite token is a reference value - a string pointing to another reference token."
|
||||
[token-value]
|
||||
(string? token-value))
|
||||
|
||||
@@ -114,25 +114,19 @@
|
||||
[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 schema:token-attrs)
|
||||
[:and {:gen/gen (->> (sg/generator cto/schema:token-attrs)
|
||||
(sg/fmap #(make-token %)))}
|
||||
(sm/required-keys schema:token-attrs)
|
||||
(sm/required-keys cto/schema:token-attrs)
|
||||
[:fn token?]])
|
||||
|
||||
(def ^:private check-token-attrs
|
||||
(sm/check-fn schema:token-attrs :hint "expected valid params for token"))
|
||||
(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))
|
||||
|
||||
(def check-token
|
||||
(sm/check-fn schema:token :hint "expected valid token"))
|
||||
@@ -317,10 +311,13 @@
|
||||
[o]
|
||||
(instance? TokenSetLegacy o))
|
||||
|
||||
(declare make-token-set)
|
||||
(declare normalized-set-name?)
|
||||
|
||||
(def schema:token-set-attrs
|
||||
[:map {:title "TokenSet"}
|
||||
[:id ::sm/uuid]
|
||||
[:name :string]
|
||||
[:name [:and :string [:fn #(normalized-set-name? %)]]]
|
||||
[:description {:optional true} :string]
|
||||
[:modified-at {:optional true} ::ct/inst]
|
||||
[:tokens {:optional true
|
||||
@@ -342,8 +339,6 @@
|
||||
: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 %)))}
|
||||
@@ -404,12 +399,25 @@
|
||||
(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.
|
||||
|
||||
@@ -1365,10 +1373,13 @@ 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 (instance? TokensLib o)
|
||||
(valid? o)))
|
||||
(and (tokens-lib? o) (valid? o)))
|
||||
|
||||
(defn- ensure-hidden-theme
|
||||
"A helper that is responsible to ensure that the hidden theme always
|
||||
@@ -1430,6 +1441,50 @@ 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 cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[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} (cft/parse-token-value "100.1")))
|
||||
(t/is (= {:value -9.0 :unit nil} (cft/parse-token-value "-9"))))
|
||||
(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/testing "trims white-space"
|
||||
(t/is (= {:value -1.3 :unit nil} (cft/parse-token-value " -1.3 "))))
|
||||
(t/is (= {:value -1.3 :unit nil} (cfo/parse-token-value " -1.3 "))))
|
||||
(t/testing "parses unit: px"
|
||||
(t/is (= {:value 70.3 :unit "px"} (cft/parse-token-value " 70.3px "))))
|
||||
(t/is (= {:value 70.3 :unit "px"} (cfo/parse-token-value " 70.3px "))))
|
||||
(t/testing "parses unit: %"
|
||||
(t/is (= {:value -10.0 :unit "%"} (cft/parse-token-value "-10%"))))
|
||||
(t/is (= {:value -10.0 :unit "%"} (cfo/parse-token-value "-10%"))))
|
||||
(t/testing "parses unit: px")
|
||||
(t/testing "returns nil for any invalid characters"
|
||||
(t/is (nil? (cft/parse-token-value " -1.3a "))))
|
||||
(t/is (nil? (cfo/parse-token-value " -1.3a "))))
|
||||
(t/testing "doesnt accept invalid double"
|
||||
(t/is (nil? (cft/parse-token-value ".3")))))
|
||||
(t/is (nil? (cfo/parse-token-value ".3")))))
|
||||
|
||||
(t/deftest token-applied-test
|
||||
(t/testing "matches passed token with `:token-attributes`"
|
||||
(t/is (true? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (true? (cfo/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match empty token"
|
||||
(t/is (nil? (cft/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "does't match passed token `:id`"
|
||||
(t/is (nil? (cft/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/is (nil? (cfo/token-applied? {:name "b"} {:applied-tokens {:x "a"}} #{:x}))))
|
||||
(t/testing "doesn't match passed `:token-attributes`"
|
||||
(t/is (nil? (cft/token-applied? {:name "a"} {:applied-tokens {:x "a"}} #{:y})))))
|
||||
(t/is (nil? (cfo/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 (cft/shapes-ids-by-applied-attributes {:name "1"} shapes attributes)]
|
||||
expected (cfo/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,34 +62,21 @@
|
||||
shape-applied-x-y
|
||||
shape-applied-all)))
|
||||
(t/is (= (:z expected) (shape-ids shape-applied-all)))
|
||||
(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)))
|
||||
(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)))
|
||||
(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? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (true? (cfo/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? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
(t/is (nil? (cfo/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "b"}}
|
||||
{:applied-tokens {:x "b"}}]
|
||||
#{:x})))
|
||||
(t/is (nil? (cft/shapes-token-applied? {:name "a"} [{:applied-tokens {:x "a"}}
|
||||
(t/is (nil? (cfo/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"}}}}))))
|
||||
|
||||
@@ -255,28 +255,28 @@
|
||||
(cls/generate-update-shapes [(:id frame1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(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])))
|
||||
(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])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id text1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(cto/unapply-token-id [:font-size])
|
||||
(cto/unapply-token-id [:letter-spacing])
|
||||
(cto/unapply-token-id [:font-family])))
|
||||
(cto/unapply-tokens-from-shape [:font-size])
|
||||
(cto/unapply-tokens-from-shape [:letter-spacing])
|
||||
(cto/unapply-tokens-from-shape [:font-family])))
|
||||
(:objects page)
|
||||
{})
|
||||
(cls/generate-update-shapes [(:id circle1)]
|
||||
(fn [shape]
|
||||
(-> shape
|
||||
(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])))
|
||||
(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])))
|
||||
(:objects page)
|
||||
{}))
|
||||
|
||||
|
||||
@@ -8,20 +8,19 @@
|
||||
(: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/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")))
|
||||
(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")))
|
||||
;; Disallow trailing tokens
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo.Bar.Baz....")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo.Bar.Baz....")))
|
||||
;; Disallow multiple separator dots
|
||||
(t/is (false? (sm/validate cto/token-name-ref "Foo..Bar.Baz")))
|
||||
(t/is (false? (sm/validate cto/schema:token-name "Foo..Bar.Baz")))
|
||||
;; Disallow any special characters
|
||||
(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"))))
|
||||
(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"))))
|
||||
|
||||
@@ -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,3 +2013,11 @@
|
||||
(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"}}}}))))
|
||||
|
||||
@@ -223,19 +223,15 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~* \.(js|css|wasm)$ {
|
||||
add_header Cache-Control "no-store" always;
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store" always;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,9 +50,4 @@ tmux select-window -t penpot:4
|
||||
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
||||
tmux send-keys -t penpot './scripts/start-dev' enter
|
||||
|
||||
tmux new-window -t penpot:5 -n 'nitrate'
|
||||
tmux select-window -t penpot:5
|
||||
tmux send-keys -t penpot 'cd penpot/penpot-nitrate' enter C-l
|
||||
tmux send-keys -t penpot 'pnpm dev --host' enter
|
||||
|
||||
tmux -2 attach-session -t penpot
|
||||
|
||||
@@ -29,9 +29,8 @@ update_flags /var/www/app/js/config.js
|
||||
|
||||
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
|
||||
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
|
||||
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
|
||||
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
|
||||
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
|
||||
|
||||
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"
|
||||
|
||||
@@ -139,14 +139,6 @@ http {
|
||||
proxy_pass $PENPOT_BACKEND_URI/ws/notifications;
|
||||
}
|
||||
|
||||
location /control-center {
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $http_host;
|
||||
proxy_set_header X-Real-IP $http_cf_connecting_ip;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_pass $PENPOT_NITRATE_URI$request_uri;
|
||||
}
|
||||
|
||||
include /etc/nginx/overrides/server.d/*.conf;
|
||||
|
||||
location / {
|
||||
|
||||
@@ -21,7 +21,6 @@
|
||||
"raw-body": "^3.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"svgo": "penpot/svgo#v3.1",
|
||||
"undici": "^7.16.0",
|
||||
"xml-js": "^1.6.11",
|
||||
"xregexp": "^5.1.2"
|
||||
},
|
||||
|
||||
@@ -7,4 +7,5 @@ bb -i '(babashka.wait/wait-for-port "localhost" 9630)';
|
||||
bb -i '(babashka.wait/wait-for-path "target/app.js")';
|
||||
sleep 2;
|
||||
|
||||
export NODE_TLS_REJECT_UNAUTHORIZED=0
|
||||
exec node target/app.js
|
||||
|
||||
@@ -107,12 +107,12 @@
|
||||
:on-progress on-progress)
|
||||
|
||||
append (fn [{:keys [filename path] :as resource}]
|
||||
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
|
||||
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
|
||||
|
||||
proc (->> exports
|
||||
(map (fn [export] (rd/render export append)))
|
||||
(p/all)
|
||||
(p/mcat (fn [_] (rsc/close-zip zip)))
|
||||
(p/fnly (fn [_] (.finalize zip)))
|
||||
(p/fmap (constantly resource))
|
||||
(p/mcat (partial rsc/upload-resource auth-token))
|
||||
(p/fmap (fn [resource]
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
["node:fs" :as fs]
|
||||
["node:fs/promises" :as fsp]
|
||||
["node:path" :as path]
|
||||
["undici" :as http]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.transit :as t]
|
||||
[app.common.uri :as u]
|
||||
@@ -54,40 +53,30 @@
|
||||
(.pipe zip out)
|
||||
zip))
|
||||
|
||||
(defn add-to-zip
|
||||
(defn add-to-zip!
|
||||
[zip path name]
|
||||
(.file ^js zip path #js {:name name}))
|
||||
|
||||
(defn close-zip
|
||||
(defn close-zip!
|
||||
[zip]
|
||||
(p/create (fn [resolve]
|
||||
(.on ^js zip "close" resolve)
|
||||
(.finalize ^js zip))))
|
||||
(.finalize ^js zip))
|
||||
|
||||
(defn upload-resource
|
||||
[auth-token resource]
|
||||
(->> (fsp/readFile (:path resource))
|
||||
(p/fmap (fn [buffer]
|
||||
(js/console.log buffer)
|
||||
(new js/Blob #js [buffer] #js {:type (:mtype resource)})))
|
||||
(p/mcat (fn [blob]
|
||||
(let [fdata (new http/FormData)
|
||||
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
||||
headers #js {"X-Shared-Key" cf/management-key
|
||||
"Authorization" (str "Bearer " auth-token)}
|
||||
|
||||
request #js {:headers headers
|
||||
:method "POST"
|
||||
:body fdata
|
||||
:dispatcher agent}
|
||||
uri (-> (cf/get :public-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "api/management/methods/upload-tempfile")
|
||||
(str))]
|
||||
|
||||
(let [fdata (new js/FormData)
|
||||
uri (-> (cf/get :public-uri)
|
||||
(u/ensure-path-slash)
|
||||
(u/join "api/management/methods/upload-tempfile")
|
||||
(str))]
|
||||
(.append fdata "content" blob (:filename resource))
|
||||
(http/fetch uri request))))
|
||||
|
||||
(js/fetch uri #js {:headers #js {"X-Shared-Key" cf/management-key
|
||||
"Authorization" (str "Bearer " auth-token)}
|
||||
:method "POST"
|
||||
:body fdata}))))
|
||||
(p/mcat (fn [response]
|
||||
(if (not= (.-status response) 200)
|
||||
(ex/raise :type :internal
|
||||
|
||||
@@ -75,8 +75,7 @@
|
||||
[path]
|
||||
(->> (.stat fs/promises path)
|
||||
(p/fmap (fn [data]
|
||||
{:path path
|
||||
:created-at (inst-ms (.-ctime ^js data))
|
||||
{:created-at (inst-ms (.-ctime ^js data))
|
||||
:size (.-size data)}))
|
||||
(p/merr (fn [_cause]
|
||||
(p/resolved nil)))))
|
||||
|
||||
@@ -582,7 +582,6 @@ __metadata:
|
||||
raw-body: "npm:^3.0.1"
|
||||
source-map-support: "npm:^0.5.21"
|
||||
svgo: "penpot/svgo#v3.1"
|
||||
undici: "npm:^7.16.0"
|
||||
ws: "npm:^8.18.3"
|
||||
xml-js: "npm:^1.6.11"
|
||||
xregexp: "npm:^5.1.2"
|
||||
@@ -1514,13 +1513,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"undici@npm:^7.16.0":
|
||||
version: 7.16.0
|
||||
resolution: "undici@npm:7.16.0"
|
||||
checksum: 10c0/efd867792e9f233facf9efa0a087e2d9c3e4415c0b234061b9b40307ca4fa01d945fee4d43c7b564e1b80e0d519bcc682f9f6e0de13c717146c00a80e2f1fb0f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"unique-filename@npm:^4.0.0":
|
||||
version: 4.0.0
|
||||
resolution: "unique-filename@npm:4.0.0"
|
||||
|
||||
@@ -50,8 +50,5 @@
|
||||
|
||||
:shadow-cljs
|
||||
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
|
||||
"-Dpenpot.wasm.profile-marks=true"
|
||||
"-XX:+UnlockExperimentalVMOptions"
|
||||
"-XX:CompileCommand=blackhole,criterium.blackhole.Blackhole::consume"]}
|
||||
:jvm-opts ["--sun-misc-unsafe-memory-access=allow" "-Dpenpot.wasm.profile-marks=true"]}
|
||||
}}
|
||||
|
||||
@@ -32,8 +32,8 @@
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c text-editor/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js text-editor/**/*.js",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
|
||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
@@ -106,7 +106,7 @@
|
||||
"@penpot/hljs": "portal:./vendor/hljs",
|
||||
"@penpot/mousetrap": "portal:./vendor/mousetrap",
|
||||
"@penpot/plugins-runtime": "1.3.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.1",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "New File 1",
|
||||
"~:revn": 11,
|
||||
"~:modified-at": "~m1713873823633",
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:is-shared": false,
|
||||
"~:version": 46,
|
||||
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
|
||||
"~:created-at": "~m1713536343369",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u66697432-c33d-8055-8006-2c62cc084cad": {
|
||||
"~#penpot/pointer": [
|
||||
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
|
||||
{
|
||||
"~:created-at": "~m1713873823636"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:options": {
|
||||
"~:components-v2": true
|
||||
},
|
||||
"~:recent-colors": [
|
||||
{
|
||||
"~:color": "#0000ff",
|
||||
"~:opacity": 1,
|
||||
"~:id": null,
|
||||
"~:file-id": null,
|
||||
"~:image": null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,345 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/pointer-map",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type",
|
||||
"text-editor/v2"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
|
||||
"~:permissions": {
|
||||
"~:type": "~:membership",
|
||||
"~:is-owner": true,
|
||||
"~:is-admin": true,
|
||||
"~:can-edit": true,
|
||||
"~:can-read": true,
|
||||
"~:is-logged": true
|
||||
},
|
||||
"~:has-media-trimmed": false,
|
||||
"~:comment-thread-seqn": 0,
|
||||
"~:name": "Bug 11552",
|
||||
"~:revn": 3,
|
||||
"~:modified-at": "~m1753957736516",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0004-clean-shadow-color",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0007-clear-invalid-strokes-and-fills-v2",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
|
||||
"~:created-at": "~m1753957644225",
|
||||
"~:data": {
|
||||
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
|
||||
"~:pages-index": {
|
||||
"~u238a17e0-75ff-8075-8006-934586ea2231": {
|
||||
"~:objects": {
|
||||
"~u00000000-0000-0000-0000-000000000000": {
|
||||
"~#shape": {
|
||||
"~:y": 0,
|
||||
"~:hide-fill-on-export": false,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:name": "Root Frame",
|
||||
"~:width": 0.01,
|
||||
"~:type": "~:frame",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.01,
|
||||
"~:y": 0.01
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 0.0,
|
||||
"~:y": 0.01
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:r2": 0,
|
||||
"~:proportion-lock": false,
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:r3": 0,
|
||||
"~:r1": 0,
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:strokes": [],
|
||||
"~:x": 0,
|
||||
"~:proportion": 1.0,
|
||||
"~:r4": 0,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 0,
|
||||
"~:y": 0,
|
||||
"~:width": 0.01,
|
||||
"~:height": 0.01,
|
||||
"~:x1": 0,
|
||||
"~:y1": 0,
|
||||
"~:x2": 0.01,
|
||||
"~:y2": 0.01
|
||||
}
|
||||
},
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#FFFFFF",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:flip-x": null,
|
||||
"~:height": 0.01,
|
||||
"~:flip-y": null,
|
||||
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
|
||||
}
|
||||
},
|
||||
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
|
||||
"~#shape": {
|
||||
"~:y": 438,
|
||||
"~:transform": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:rotation": 0,
|
||||
"~:grow-type": "~:auto-width",
|
||||
"~:content": {
|
||||
"~:type": "root",
|
||||
"~:key": "1s4am1jl24s",
|
||||
"~:children": [
|
||||
{
|
||||
"~:type": "paragraph-set",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:children": [
|
||||
{
|
||||
"~:line-height": "1.2",
|
||||
"~:font-style": "normal",
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "13p0zwl2yhc",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro",
|
||||
"~:text": "Lorem ipsum"
|
||||
}
|
||||
],
|
||||
"~:typography-ref-id": null,
|
||||
"~:text-transform": "none",
|
||||
"~:text-align": "left",
|
||||
"~:font-id": "sourcesanspro",
|
||||
"~:key": "20hf3kmyoub",
|
||||
"~:font-size": "14",
|
||||
"~:font-weight": "400",
|
||||
"~:typography-ref-file": null,
|
||||
"~:text-direction": "ltr",
|
||||
"~:type": "paragraph",
|
||||
"~:font-variant-id": "regular",
|
||||
"~:text-decoration": "none",
|
||||
"~:letter-spacing": "0",
|
||||
"~:fills": [
|
||||
{
|
||||
"~:fill-color": "#000000",
|
||||
"~:fill-opacity": 1
|
||||
}
|
||||
],
|
||||
"~:font-family": "sourcesanspro"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"~:vertical-align": "top"
|
||||
},
|
||||
"~:hide-in-viewer": false,
|
||||
"~:name": "Lorem ipsum",
|
||||
"~:width": 77,
|
||||
"~:type": "~:text",
|
||||
"~:points": [
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 438
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 481,
|
||||
"~:y": 455
|
||||
}
|
||||
},
|
||||
{
|
||||
"~#point": {
|
||||
"~:x": 404,
|
||||
"~:y": 455
|
||||
}
|
||||
}
|
||||
],
|
||||
"~:transform-inverse": {
|
||||
"~#matrix": {
|
||||
"~:a": 1.0,
|
||||
"~:b": 0.0,
|
||||
"~:c": 0.0,
|
||||
"~:d": 1.0,
|
||||
"~:e": 0.0,
|
||||
"~:f": 0.0
|
||||
}
|
||||
},
|
||||
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
|
||||
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:x": 404,
|
||||
"~:selrect": {
|
||||
"~#rect": {
|
||||
"~:x": 404,
|
||||
"~:y": 438,
|
||||
"~:width": 77,
|
||||
"~:height": 17,
|
||||
"~:x1": 404,
|
||||
"~:y1": 438,
|
||||
"~:x2": 481,
|
||||
"~:y2": 455
|
||||
}
|
||||
},
|
||||
"~:flip-x": null,
|
||||
"~:height": 17,
|
||||
"~:flip-y": null
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +1,5 @@
|
||||
w
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"~:revn": 2,
|
||||
"~:lagged": []
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
[
|
||||
{
|
||||
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
|
||||
"~:revn": 21,
|
||||
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
|
||||
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
|
||||
"~:changes": []
|
||||
}
|
||||
]
|
||||
@@ -1,36 +0,0 @@
|
||||
export class Clipboard {
|
||||
static Permission = {
|
||||
ONLY_READ: ["clipboard-read"],
|
||||
ONLY_WRITE: ["clipboard-write"],
|
||||
ALL: ["clipboard-read", "clipboard-write"],
|
||||
};
|
||||
|
||||
static enable(context, permissions) {
|
||||
return context.grantPermissions(permissions);
|
||||
}
|
||||
|
||||
static writeText(page, text) {
|
||||
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
|
||||
}
|
||||
|
||||
static readText(page) {
|
||||
return page.evaluate(() => navigator.clipboard.readText());
|
||||
}
|
||||
|
||||
constructor(page, context) {
|
||||
this.page = page;
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
enable(permissions) {
|
||||
return Clipboard.enable(this.context, permissions);
|
||||
}
|
||||
|
||||
writeText(text) {
|
||||
return Clipboard.writeText(this.page, text);
|
||||
}
|
||||
|
||||
readText() {
|
||||
return Clipboard.readText(this.page);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
export class Transit {
|
||||
static parse(value) {
|
||||
if (typeof value !== "string") return value;
|
||||
|
||||
if (value.startsWith("~")) return value.slice(2);
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
static get(object, ...path) {
|
||||
let aux = object;
|
||||
for (const name of path) {
|
||||
if (typeof name !== "string") {
|
||||
if (!(name in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[name];
|
||||
} else {
|
||||
const transitName = `~:${name}`;
|
||||
if (!(transitName in aux)) {
|
||||
return undefined;
|
||||
}
|
||||
aux = aux[transitName];
|
||||
}
|
||||
}
|
||||
return this.parse(aux);
|
||||
}
|
||||
}
|
||||
@@ -1,27 +1,4 @@
|
||||
export class BasePage {
|
||||
/**
|
||||
* Mocks multiple RPC calls in a single call.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {object<string, string>} paths
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPCs(page, paths, options) {
|
||||
for (const [path, jsonFilename] of Object.entries(paths)) {
|
||||
await this.mockRPC(page, path, jsonFilename, options);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Mocks an RPC call using a file.
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {string} path
|
||||
* @param {string} jsonFilename
|
||||
* @param {*} options
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
static async mockRPC(page, path, jsonFilename, options) {
|
||||
if (!page) {
|
||||
throw new TypeError("Invalid page argument. Must be a Playwright page.");
|
||||
@@ -116,10 +93,6 @@ export class BasePage {
|
||||
return this.#page;
|
||||
}
|
||||
|
||||
async mockRPCs(paths, options) {
|
||||
return BasePage.mockRPCs(this.page, paths, options);
|
||||
}
|
||||
|
||||
async mockRPC(path, jsonFilename, options) {
|
||||
return BasePage.mockRPC(this.page, path, jsonFilename, options);
|
||||
}
|
||||
|
||||
@@ -1,146 +1,7 @@
|
||||
import { expect } from "@playwright/test";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { BaseWebSocketPage } from "./BaseWebSocketPage";
|
||||
import { Transit } from "../../helpers/Transit";
|
||||
|
||||
export class WorkspacePage extends BaseWebSocketPage {
|
||||
static TextEditor = class TextEditor {
|
||||
constructor(workspacePage) {
|
||||
this.workspacePage = workspacePage;
|
||||
|
||||
// locators.
|
||||
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
|
||||
name: "Line Height",
|
||||
});
|
||||
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
|
||||
"textbox",
|
||||
{
|
||||
name: "Letter Spacing",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
get page() {
|
||||
return this.workspacePage.page;
|
||||
}
|
||||
|
||||
async waitForStyle(locator, styleName) {
|
||||
return locator.evaluate(
|
||||
(element, styleName) => element.style.getPropertyValue(styleName),
|
||||
styleName,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForEditor() {
|
||||
return this.page.waitForSelector('[data-itype="editor"]');
|
||||
}
|
||||
|
||||
async waitForRoot() {
|
||||
return this.page.waitForSelector('[data-itype="root"]');
|
||||
}
|
||||
|
||||
async waitForParagraph(nth) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="paragraph"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="paragraph"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForParagraphStyle(nth, styleName) {
|
||||
const paragraph = await this.waitForParagraph(nth);
|
||||
return this.waitForStyle(paragraph, styleName);
|
||||
}
|
||||
|
||||
async waitForTextSpan(nth = 0) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="inline"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="inline"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
async waitForTextSpanContent(nth = 0) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
const textContent = await textSpan.textContent();
|
||||
return textContent;
|
||||
}
|
||||
|
||||
async waitForTextSpanStyle(nth, styleName) {
|
||||
const textSpan = await this.waitForTextSpan(nth);
|
||||
return this.waitForStyle(textSpan, styleName);
|
||||
}
|
||||
|
||||
async startEditing() {
|
||||
await this.page.keyboard.press("Enter");
|
||||
return this.waitForEditor();
|
||||
}
|
||||
|
||||
stopEditing() {
|
||||
return this.page.keyboard.press("Escape");
|
||||
}
|
||||
|
||||
async moveToLeft(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
}
|
||||
}
|
||||
|
||||
async moveToRight(amount = 0) {
|
||||
for (let i = 0; i < amount; i++) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
}
|
||||
}
|
||||
|
||||
async moveFromStart(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowLeft");
|
||||
await this.moveToRight(offset);
|
||||
}
|
||||
|
||||
async moveFromEnd(offset = 0) {
|
||||
await this.page.keyboard.press("ArrowRight");
|
||||
await this.moveToLeft(offset);
|
||||
}
|
||||
|
||||
async selectFromStart(length, offset = 0) {
|
||||
await this.moveFromStart(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToRight(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async selectFromEnd(length, offset = 0) {
|
||||
await this.moveFromEnd(offset);
|
||||
await this.page.keyboard.down("Shift");
|
||||
await this.moveToLeft(length);
|
||||
await this.page.keyboard.up("Shift");
|
||||
}
|
||||
|
||||
async changeNumericInput(locator, newValue) {
|
||||
await expect(locator).toBeVisible();
|
||||
await locator.focus();
|
||||
await locator.fill(`${newValue}`);
|
||||
await locator.blur();
|
||||
}
|
||||
|
||||
changeFontSize(newValue) {
|
||||
return this.changeNumericInput(this.fontSize, newValue);
|
||||
}
|
||||
|
||||
changeLineHeight(newValue) {
|
||||
return this.changeNumericInput(this.lineHeight, newValue);
|
||||
}
|
||||
|
||||
changeLetterSpacing(newValue) {
|
||||
return this.changeNumericInput(this.letterSpacing, newValue);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This should be called on `test.beforeEach`.
|
||||
*
|
||||
@@ -150,21 +11,50 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static async init(page) {
|
||||
await BaseWebSocketPage.initWebSockets(page);
|
||||
|
||||
await BaseWebSocketPage.mockRPCs(page, {
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-teams": "get-teams.json",
|
||||
"get-team-members?team-id=*":
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"update-profile-props": "workspace/update-profile-empty.json",
|
||||
});
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team?id=*",
|
||||
"workspace/get-team-default.json",
|
||||
);
|
||||
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-team-members?team-id=*",
|
||||
"logged-in-user/get-team-members-your-penpot.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
|
||||
await BaseWebSocketPage.mockRPC(
|
||||
page,
|
||||
"update-profile-props",
|
||||
"workspace/update-profile-empty.json",
|
||||
);
|
||||
}
|
||||
|
||||
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
|
||||
@@ -172,20 +62,9 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
|
||||
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
|
||||
|
||||
/**
|
||||
* WebSocket mock
|
||||
*
|
||||
* @type {MockWebSocketHelper}
|
||||
*/
|
||||
#ws = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {Page} page
|
||||
* @param {} [options]
|
||||
*/
|
||||
constructor(page, options) {
|
||||
constructor(page) {
|
||||
super(page);
|
||||
this.pageName = page.getByTestId("page-name");
|
||||
|
||||
@@ -233,14 +112,11 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
"tokens-context-menu-for-set",
|
||||
);
|
||||
this.contextMenuForShape = page.getByTestId("context-menu");
|
||||
if (options?.textEditor) {
|
||||
this.textEditor = new WorkspacePage.TextEditor(this);
|
||||
}
|
||||
}
|
||||
|
||||
async goToWorkspace({
|
||||
fileId = this.fileId ?? WorkspacePage.anyFileId,
|
||||
pageId = this.pageId ?? WorkspacePage.anyPageId,
|
||||
fileId = WorkspacePage.anyFileId,
|
||||
pageId = WorkspacePage.anyPageId,
|
||||
} = {}) {
|
||||
await this.page.goto(
|
||||
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
|
||||
@@ -265,59 +141,48 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupEmptyFile() {
|
||||
await this.mockRPCs({
|
||||
"get-profile": "logged-in-user/get-profile-logged-in.json",
|
||||
"get-team-users?file-id=*":
|
||||
"logged-in-user/get-team-users-single-user.json ",
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
"get-project?id=*": "workspace/get-project-default.json",
|
||||
"get-team?id=*": "workspace/get-team-default.json",
|
||||
"get-profiles-for-file-comments?file-id=*":
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
"get-file-object-thumbnails?file-id=*":
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
|
||||
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
|
||||
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
|
||||
});
|
||||
|
||||
if (this.textEditor) {
|
||||
await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
}
|
||||
|
||||
// by default we mock the blank file.
|
||||
await this.mockGetFile("workspace/get-file-blank.json");
|
||||
await this.mockRPC(
|
||||
"get-profile",
|
||||
"logged-in-user/get-profile-logged-in.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-team-users?file-id=*",
|
||||
"logged-in-user/get-team-users-single-user.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-project?id=*",
|
||||
"workspace/get-project-default.json",
|
||||
);
|
||||
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
|
||||
await this.mockRPC(
|
||||
"get-profiles-for-file-comments?file-id=*",
|
||||
"workspace/get-profile-for-file-comments.json",
|
||||
);
|
||||
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json");
|
||||
await this.mockRPC(
|
||||
"get-file-object-thumbnails?file-id=*",
|
||||
"workspace/get-file-object-thumbnails-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-font-variants?team-id=*",
|
||||
"workspace/get-font-variants-empty.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*",
|
||||
"workspace/get-file-fragment-blank.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-libraries?file-id=*",
|
||||
"workspace/get-file-libraries-empty.json",
|
||||
);
|
||||
}
|
||||
|
||||
async mockGetFile(jsonFilename, options) {
|
||||
const page = this.page;
|
||||
const jsonPath = `playwright/data/${jsonFilename}`;
|
||||
const body = await readFile(jsonPath, "utf-8");
|
||||
const payload = JSON.parse(body);
|
||||
|
||||
const fileId = Transit.get(payload, "id");
|
||||
const pageId = Transit.get(payload, "data", "pages", 0);
|
||||
const teamId = Transit.get(payload, "team-id");
|
||||
|
||||
this.fileId = fileId ?? this.anyFileId;
|
||||
this.pageId = pageId ?? this.anyPageId;
|
||||
this.teamId = teamId ?? this.anyTeamId;
|
||||
|
||||
const path = /get\-file\?/;
|
||||
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
|
||||
const interceptConfig = {
|
||||
status: 200,
|
||||
contentType: "application/transit+json",
|
||||
...options,
|
||||
};
|
||||
return page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
...interceptConfig,
|
||||
body,
|
||||
}),
|
||||
);
|
||||
// await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
async mockGetFile(jsonFile) {
|
||||
await this.mockRPC(/get\-file\?/, jsonFile);
|
||||
}
|
||||
|
||||
async mockGetAsset(regex, asset) {
|
||||
@@ -325,15 +190,22 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
}
|
||||
|
||||
async setupFileWithComments() {
|
||||
await this.mockRPCs({
|
||||
"get-comment-threads?file-id=*":
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
"get-file-fragment?file-id=*&fragment-id=*":
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
|
||||
"update-comment-thread-status":
|
||||
"workspace/update-comment-thread-status.json",
|
||||
});
|
||||
await this.mockRPC(
|
||||
"get-comment-threads?file-id=*",
|
||||
"workspace/get-comment-threads-unread.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-file-fragment?file-id=*&fragment-id=*",
|
||||
"viewer/get-file-fragment-single-board.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"get-comments?thread-id=*",
|
||||
"workspace/get-thread-comments.json",
|
||||
);
|
||||
await this.mockRPC(
|
||||
"update-comment-thread-status",
|
||||
"workspace/update-comment-thread-status.json",
|
||||
);
|
||||
}
|
||||
|
||||
async clickWithDragViewportAt(x, y, width, height) {
|
||||
@@ -351,67 +223,6 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks and moves from the coordinates x1,y1 to x2,y2
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
*/
|
||||
async clickAndMove(x1, y1, x2, y2) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x: x1, y: y1 } });
|
||||
await this.page.mouse.down();
|
||||
await this.viewport.hover({ position: { x: x2, y: y2 } });
|
||||
await this.page.mouse.up();
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Text Shape in the specified coordinates
|
||||
* with an initial text.
|
||||
*
|
||||
* @param {number} x1
|
||||
* @param {number} y1
|
||||
* @param {number} x2
|
||||
* @param {number} y2
|
||||
* @param {string} initialText
|
||||
* @param {*} [options]
|
||||
*/
|
||||
async createTextShape(x1, y1, x2, y2, initialText, options) {
|
||||
const timeToWait = options?.timeToWait ?? 100;
|
||||
await this.page.keyboard.press("T");
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
await this.clickAndMove(x1, y1, x2, y2);
|
||||
await this.page.waitForTimeout(timeToWait);
|
||||
if (initialText) {
|
||||
await this.page.keyboard.type(initialText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies the selected element into the clipboard.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async copy() {
|
||||
return this.page.keyboard.press("Control+C");
|
||||
}
|
||||
|
||||
/**
|
||||
* Pastes something from the clipboard.
|
||||
*
|
||||
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async paste(kind = "keyboard") {
|
||||
if (kind === "context-menu") {
|
||||
await this.viewport.click({ button: "right" });
|
||||
return this.page.getByText("PasteCtrlV").click();
|
||||
}
|
||||
return this.page.keyboard.press("Control+V");
|
||||
}
|
||||
|
||||
async panOnViewportAt(x, y, width, height) {
|
||||
await this.page.waitForTimeout(100);
|
||||
await this.viewport.hover({ position: { x, y } });
|
||||
@@ -439,15 +250,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
await this.page.waitForTimeout(500);
|
||||
}
|
||||
|
||||
async doubleClickLeafLayer(name, clickOptions = {}) {
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
await this.clickLeafLayer(name, clickOptions);
|
||||
}
|
||||
|
||||
async clickToggableLayer(name, clickOptions = {}) {
|
||||
const layer = this.layers
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
.getByTestId("layer-row")
|
||||
.filter({ hasText: name });
|
||||
const button = layer.getByRole("button");
|
||||
|
||||
await button.waitFor();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 24 KiB |
@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
|
||||
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
|
||||
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
await workspace.waitForFirstRender();
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
|
||||
@@ -18,10 +18,6 @@ const setupFile = async (workspacePage) => {
|
||||
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
|
||||
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
|
||||
});
|
||||
await workspacePage.mockRPC(
|
||||
"update-file?id=*",
|
||||
"workspace/update-file-empty.json",
|
||||
);
|
||||
};
|
||||
|
||||
const shapeToLayerName = {
|
||||
|
||||
@@ -51,7 +51,7 @@ test.skip("BUG 12164 - Crash when trying to fetch a missing font", async ({
|
||||
pageId: "2b7f0188-51a1-8193-8006-e05bad87b74d",
|
||||
});
|
||||
|
||||
await workspacePage.page.waitForTimeout(1000);
|
||||
await workspacePage.page.waitForTimeout(1000)
|
||||
await workspacePage.waitForFirstRender();
|
||||
|
||||
await expect(
|
||||
|
||||
@@ -1,323 +1,12 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { Clipboard } from "../../helpers/Clipboard";
|
||||
import { WorkspacePage } from "../pages/WorkspacePage";
|
||||
|
||||
const timeToWait = 100;
|
||||
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await WorkspacePage.init(page);
|
||||
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ context }) => {
|
||||
context.clearPermissions();
|
||||
});
|
||||
|
||||
test("Create a new text shape", async ({ page }) => {
|
||||
const initialText = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.createTextShape(190, 150, 300, 200, initialText);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(initialText);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text", async ({ page, context }) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
await page.waitForTimeout(timeToWait);
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Create a new text shape from pasting text using context menu", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Lorem ipsum";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickAt(190, 150);
|
||||
await workspace.paste("context-menu");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe(textToPaste);
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by appending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd(0);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by prepending text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(0);
|
||||
await page.keyboard.type("Dolor sit amet ");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update an already created text shape by inserting text in between", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart(5);
|
||||
await page.keyboard.type(" dolor sit amet");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape appending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = " dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromEnd();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum dolor sit amet");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape prepending text by pasting text", async ({
|
||||
page,
|
||||
context,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet ";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.moveFromStart();
|
||||
await workspace.paste("keyboard");
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (starting) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "Dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Dolor sit amet ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (ending) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromEnd(5);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem dolor sit amet");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update a new text shape replacing (in between) text with pasted text", async ({
|
||||
page,
|
||||
}) => {
|
||||
const textToPaste = "dolor sit amet";
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5, 3);
|
||||
|
||||
await Clipboard.writeText(page, textToPaste);
|
||||
|
||||
await workspace.paste("keyboard");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lordolor sit ametsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("Update text font size selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeFontSize(36);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text line height selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLineHeight(1.4);
|
||||
|
||||
const lineHeight = await workspace.textEditor.waitForParagraphStyle(
|
||||
1,
|
||||
"line-height",
|
||||
);
|
||||
expect(lineHeight).toBe("1.4");
|
||||
|
||||
const textContent = await workspace.textEditor.waitForTextSpanContent();
|
||||
expect(textContent).toBe("Lorem ipsum");
|
||||
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
|
||||
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.textEditor.startEditing();
|
||||
await workspace.textEditor.selectFromStart(5);
|
||||
await workspace.textEditor.changeLetterSpacing(10);
|
||||
|
||||
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
|
||||
expect(textContent1).toBe("Lorem");
|
||||
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
|
||||
expect(textContent2).toBe(" ipsum");
|
||||
await workspace.textEditor.stopEditing();
|
||||
});
|
||||
|
||||
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("text-editor/get-file-11552.json");
|
||||
@@ -325,16 +14,21 @@ test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
|
||||
"update-file?id=*",
|
||||
"text-editor/update-file-11552.json",
|
||||
);
|
||||
await workspace.goToWorkspace();
|
||||
await workspace.doubleClickLeafLayer("Lorem ipsum");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
|
||||
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
|
||||
});
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
await workspace.clickLeafLayer("Lorem ipsum");
|
||||
|
||||
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
|
||||
name: "Font Size",
|
||||
});
|
||||
await expect(fontSizeInput).toBeVisible();
|
||||
|
||||
await page.keyboard.press("Enter");
|
||||
await page.keyboard.press("ArrowRight");
|
||||
await workspace.page.keyboard.press("Enter");
|
||||
await workspace.page.keyboard.press("ArrowRight");
|
||||
|
||||
await fontSizeInput.fill("36");
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -181,8 +181,8 @@ export async function watch(baseDir, predicate, callback) {
|
||||
});
|
||||
}
|
||||
|
||||
async function readManifestFile(resource) {
|
||||
const manifestPath = "resources/public/" + resource;
|
||||
async function readManifestFile() {
|
||||
const manifestPath = "resources/public/js/manifest.json";
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
return JSON.parse(content);
|
||||
}
|
||||
|
||||
@@ -20,26 +20,27 @@ echo $PATH
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
rm -rf target/dist;
|
||||
rm -rf resources/public;
|
||||
rm -rf target/dist;
|
||||
|
||||
mkdir -p resources/public;
|
||||
mkdir -p target/dist;
|
||||
|
||||
pushd ../render-wasm;
|
||||
./build
|
||||
popd
|
||||
yarn run build:app:main $EXTRA_PARAMS || exit 1
|
||||
|
||||
yarn run build:app:main $EXTRA_PARAMS;
|
||||
yarn run build:app:libs;
|
||||
yarn run build:app:assets;
|
||||
if [ "$INCLUDE_WASM" = "yes" ]; then
|
||||
yarn run build:wasm || exit 1;
|
||||
fi
|
||||
|
||||
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js
|
||||
yarn run build:app:libs || exit 1;
|
||||
yarn run build:app:assets || exit 1;
|
||||
|
||||
rsync -avr resources/public/ target/dist/
|
||||
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
|
||||
|
||||
rsync -avr resources/public/ target/dist/;
|
||||
|
||||
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
|
||||
# build storybook
|
||||
|
||||
@@ -81,7 +81,7 @@
|
||||
:source-map-detail-level :all}}}
|
||||
|
||||
:worker
|
||||
{:target :browser
|
||||
{:target :esm
|
||||
:output-dir "resources/public/js/worker/"
|
||||
:asset-path "/js/worker"
|
||||
:devtools {:browser-inject :main
|
||||
@@ -92,7 +92,6 @@
|
||||
{:main
|
||||
{:entries [app.worker]
|
||||
:web-worker true
|
||||
:prepend-js "importScripts('./render.js');"
|
||||
:depends-on #{}}}
|
||||
|
||||
:js-options
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
public-uri))
|
||||
|
||||
(def worker-uri
|
||||
(obj/get global "penpotWorkerURI" "/js/worker/main.js"))
|
||||
(obj/get global "penpotWorkerURI" "/js/worker.js"))
|
||||
|
||||
(defn external-feature-flag
|
||||
[flag value]
|
||||
@@ -188,11 +188,6 @@
|
||||
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
|
||||
(false? thumbnail?) (u/join (dm/str id)))))))
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(u/join public-uri path))
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
(map :page-id))
|
||||
|
||||
(defn- apply-changes-localy
|
||||
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
|
||||
[{:keys [file-id redo-changes] :as commit} pending]
|
||||
(ptk/reify ::apply-changes-localy
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -103,7 +103,7 @@
|
||||
pids (into #{} xf:map-page-id redo-changes)]
|
||||
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
|
||||
|
||||
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
;; Update the wasm model
|
||||
(let [shape-changes (volatile! {})
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
(defn commit
|
||||
"Create a commit event instance"
|
||||
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
|
||||
file-id file-revn file-vern undo-group tags stack-undo? source]}]
|
||||
|
||||
(assert (cpc/check-changes redo-changes)
|
||||
"expect valid vector of changes for redo-changes")
|
||||
@@ -147,8 +147,7 @@
|
||||
:save-undo? save-undo?
|
||||
:undo-group undo-group
|
||||
:tags tags
|
||||
:stack-undo? stack-undo?
|
||||
:ignore-wasm? ignore-wasm?}]
|
||||
:stack-undo? stack-undo?}]
|
||||
|
||||
(ptk/reify ::commit
|
||||
cljs.core/IDeref
|
||||
|
||||
@@ -649,22 +649,10 @@
|
||||
(rx/of (dcm/change-team-role params)
|
||||
(modal/hide)))))
|
||||
|
||||
(defn handle-change-team-org
|
||||
[{:keys [team-id organization-id organization-name] :as message}]
|
||||
(ptk/reify ::handle-change-team-org
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(if (contains? (:teams state) team-id)
|
||||
(-> state
|
||||
(assoc-in [:teams team-id :organization-id] organization-id)
|
||||
(assoc-in [:teams team-id :organization-name] organization-name))
|
||||
state))))
|
||||
|
||||
(defn- process-message
|
||||
[{:keys [type] :as msg}]
|
||||
(case type
|
||||
:notification (dcm/handle-notification msg)
|
||||
:team-role-change (handle-change-team-role msg)
|
||||
:team-membership-change (dcm/team-membership-change msg)
|
||||
:team-org-change (handle-change-team-org msg)
|
||||
nil))
|
||||
|
||||
@@ -24,8 +24,6 @@
|
||||
(def revn-data (atom {}))
|
||||
(def queue-conj (fnil conj #queue []))
|
||||
|
||||
(def force-persist? #(= % ::force-persist))
|
||||
|
||||
(defn- update-status
|
||||
[status]
|
||||
(ptk/reify ::update-status
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
(:require
|
||||
["@tokens-studio/sd-transforms" :as sd-transforms]
|
||||
["style-dictionary$default" :as sd]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[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 (cft/parse-token-value value)
|
||||
parsed-value (cfo/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 (cft/parse-token-value value)
|
||||
(let [parsed-value (cfo/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 (cft/parse-token-value value)
|
||||
parsed-value (cfo/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 (cft/parse-token-value value)
|
||||
parsed-value (cfo/parse-token-value value)
|
||||
out-of-scope (< (:value parsed-value) 0)
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
(cond
|
||||
@@ -250,7 +250,7 @@
|
||||
:font-size-value font-size-value})]
|
||||
(or error
|
||||
(try
|
||||
(when-let [{:keys [unit value]} (cft/parse-token-value line-height-value)]
|
||||
(when-let [{:keys [unit value]} (cfo/parse-token-value line-height-value)]
|
||||
(case unit
|
||||
"%" (/ value 100)
|
||||
"px" (/ value font-size-value)
|
||||
@@ -261,19 +261,14 @@
|
||||
|
||||
(defn- parse-sd-token-font-family-value
|
||||
[value]
|
||||
(let [value (-> (js->clj value) (flatten))
|
||||
valid-font-family (or (string? value) (every? string? value))
|
||||
missing-references (seq (some cto/find-token-value-references value))]
|
||||
(let [missing-references (seq (some cto/find-token-value-references value))]
|
||||
(cond
|
||||
(not valid-font-family)
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)]}
|
||||
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
|
||||
:else
|
||||
{:value value})))
|
||||
{:value (-> (js->clj value) (flatten))})))
|
||||
|
||||
(defn parse-atomic-typography-value [token-type token-value]
|
||||
(case token-type
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.notifications :as ntf]
|
||||
[app.main.data.persistence :as dps]
|
||||
[app.main.data.persistence :as-alias dps]
|
||||
[app.main.data.plugins :as dp]
|
||||
[app.main.data.profile :as du]
|
||||
[app.main.data.project :as dpj]
|
||||
@@ -67,7 +67,6 @@
|
||||
[app.main.errors]
|
||||
[app.main.features :as features]
|
||||
[app.main.features.pointer-map :as fpmap]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.repo :as rp]
|
||||
[app.main.router :as rt]
|
||||
[app.render-wasm :as wasm]
|
||||
@@ -380,59 +379,6 @@
|
||||
(->> (rx/from added)
|
||||
(rx/map process-wasm-object)))))))
|
||||
|
||||
(when render-wasm?
|
||||
(let [local-commits-s
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
(rx/filter #(and (= :local (:source %))
|
||||
(not (contains? (:tags %) :position-data))))
|
||||
(rx/filter (complement empty?)))
|
||||
|
||||
notifier-s
|
||||
(rx/merge
|
||||
(->> local-commits-s (rx/debounce 1000))
|
||||
(->> stream (rx/filter dps/force-persist?)))
|
||||
|
||||
objects-s
|
||||
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
||||
|
||||
current-page-id-s
|
||||
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
||||
|
||||
(->> local-commits-s
|
||||
(rx/buffer-until notifier-s)
|
||||
(rx/with-latest-from objects-s)
|
||||
(rx/map
|
||||
(fn [[commits objects]]
|
||||
(->> commits
|
||||
(mapcat :redo-changes)
|
||||
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
||||
(filter #(cfh/text-shape? objects (:id %)))
|
||||
(map #(vector
|
||||
(:id %)
|
||||
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
||||
|
||||
(rx/with-latest-from current-page-id-s)
|
||||
(rx/map
|
||||
(fn [[text-position-data page-id]]
|
||||
(let [changes
|
||||
(->> text-position-data
|
||||
(mapv (fn [[id position-data]]
|
||||
{:type :mod-obj
|
||||
:id id
|
||||
:page-id page-id
|
||||
:operations
|
||||
[{:type :set
|
||||
:attr :position-data
|
||||
:val position-data
|
||||
:ignore-touched true
|
||||
:ignore-geometry true}]})))]
|
||||
(dch/commit-changes
|
||||
{:redo-changes changes :undo-changes []
|
||||
:save-undo? false
|
||||
:tags #{:position-data}})))))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter dch/commit?)
|
||||
(rx/map deref)
|
||||
|
||||
@@ -102,8 +102,7 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
;; FIXME: would be nice to not execute this code twice per page in the
|
||||
;; same working session, maybe some local memoization can improve that
|
||||
@@ -120,5 +119,4 @@
|
||||
{:origin it
|
||||
:redo-changes changes
|
||||
:undo-changes []
|
||||
:save-undo? false
|
||||
:ignore-wasm? true})))))))
|
||||
:save-undo? false})))))))
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
|
||||
|
||||
ids
|
||||
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
|
||||
(into [] xf:without-uuid-zero (keys transforms))
|
||||
|
||||
update-shape
|
||||
(fn [shape]
|
||||
|
||||
@@ -831,8 +831,7 @@
|
||||
(effect [_ state _]
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(let [instance (:workspace-editor state)
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
|
||||
(styles/get-styles-from-style-declaration))
|
||||
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration))
|
||||
overriden-attrs (merge attrs-to-override attrs)
|
||||
styles (styles/attrs->styles overriden-attrs)]
|
||||
(editor.v2/applyStylesToSelection instance styles))))))
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
(ns app.main.data.workspace.tokens.application
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[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 [(cft/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cft/attributes-map attributes token)
|
||||
resolved-value (get-in resolved-tokens [(cfo/token-identifier token) :resolved-value])
|
||||
tokenized-attributes (cfo/attributes-map attributes token)
|
||||
type (:type token)]
|
||||
(rx/concat
|
||||
(rx/of
|
||||
@@ -585,7 +585,7 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(rx/of
|
||||
(let [remove-token #(when % (cft/remove-attributes-for-token attributes token %))]
|
||||
(let [remove-token #(when % (cfo/remove-attributes-for-token attributes token %))]
|
||||
(dwsh/update-shapes
|
||||
shape-ids
|
||||
(fn [shape]
|
||||
@@ -613,7 +613,7 @@
|
||||
(get token-properties (:type token))
|
||||
|
||||
unapply-tokens?
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
(cfo/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 cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[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 (cft/color-token? token))
|
||||
(when (and resolved-value (cfo/color-token? token))
|
||||
(color-bullet-color resolved-value)))
|
||||
@@ -88,10 +88,6 @@
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-weight
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-font-family
|
||||
{:error/code :error.style-dictionary/invalid-token-value-font-family
|
||||
:error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)}
|
||||
|
||||
:error.style-dictionary/invalid-token-value-typography
|
||||
{:error/code :error.style-dictionary/invalid-token-value-typography
|
||||
:error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}
|
||||
|
||||
@@ -147,27 +147,30 @@
|
||||
|
||||
(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/UpdateEvent
|
||||
(update [_ state]
|
||||
;; Clear possible local state
|
||||
(update state :workspace-tokens dissoc :token-set-new-path))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(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))))))))
|
||||
(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))))))
|
||||
|
||||
(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))))))
|
||||
|
||||
(defn rename-token-set-group
|
||||
[set-group-path set-group-fname]
|
||||
@@ -179,26 +182,6 @@
|
||||
(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
|
||||
@@ -448,7 +431,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)
|
||||
unames (map :name tokens) ;; TODO: add function duplicate-token in tokens-lib
|
||||
suffix (tr "workspace.tokens.duplicate-suffix")
|
||||
copy-name (cfh/generate-unique-name (:name token) unames :suffix suffix)
|
||||
new-token (-> token
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
(log/set-level! :warn)
|
||||
|
||||
(def google-fonts
|
||||
(preload-gfonts "fonts/gfonts.2025.11.28.json"))
|
||||
(preload-gfonts "fonts/gfonts.2025.05.19.json"))
|
||||
|
||||
(def local-fonts
|
||||
[{:id "sourcesanspro"
|
||||
@@ -342,8 +342,8 @@
|
||||
(fn [result {:keys [font-id] :as node}]
|
||||
(let [current-font
|
||||
(if (some? font-id)
|
||||
(select-keys node [:font-id :font-variant-id :font-weight :font-style])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id :font-weight :font-style]))]
|
||||
(select-keys node [:font-id :font-variant-id])
|
||||
(select-keys txt/default-typography [:font-id :font-variant-id]))]
|
||||
(conj result current-font)))
|
||||
#{})))
|
||||
|
||||
|
||||
@@ -30,9 +30,6 @@
|
||||
(def profile
|
||||
(l/derived (l/key :profile) st/state))
|
||||
|
||||
(def current-page-id
|
||||
(l/derived (l/key :current-page-id) st/state))
|
||||
|
||||
(def team
|
||||
(l/derived (fn [state]
|
||||
(let [team-id (:current-team-id state)
|
||||
@@ -375,9 +372,6 @@
|
||||
(def workspace-modifiers
|
||||
(l/derived :workspace-modifiers st/state))
|
||||
|
||||
(def workspace-wasm-modifiers
|
||||
(l/derived :workspace-wasm-modifiers st/state))
|
||||
|
||||
(def ^:private workspace-modifiers-with-objects
|
||||
(l/derived
|
||||
(fn [state]
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
current-id (get state :id)
|
||||
current-value (get state :current-value)
|
||||
current-label (get label-index current-value)
|
||||
|
||||
is-open? (get state :is-open?)
|
||||
|
||||
node-ref (mf/use-ref nil)
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
(def current-zoom (mf/create-context nil))
|
||||
|
||||
(def workspace-read-only? (mf/create-context nil))
|
||||
(def is-render? (mf/create-context false))
|
||||
(def is-component? (mf/create-context false))
|
||||
|
||||
(def sidebar
|
||||
|
||||
@@ -280,8 +280,8 @@
|
||||
|
||||
(mf/defc teams-selector-dropdown*
|
||||
{::mf/private true}
|
||||
[{:keys [team profile teams show-default-team allow-create-teams allow-create-org] :rest props}]
|
||||
(let [on-create-team-click
|
||||
[{:keys [team profile teams] :rest props}]
|
||||
(let [on-create-click
|
||||
(mf/use-fn #(st/emit! (modal/show :team-form {})))
|
||||
|
||||
on-team-click
|
||||
@@ -290,25 +290,18 @@
|
||||
(let [team-id (-> (dom/get-current-target event)
|
||||
(dom/get-data "value")
|
||||
(uuid/parse))]
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))
|
||||
|
||||
on-create-org-click
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
;; TODO update when org creation route is ready
|
||||
(dom/open-new-window "localhost:3000/org/create")))]
|
||||
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
|
||||
|
||||
[:> dropdown-menu* props
|
||||
|
||||
(when show-default-team
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
:data-value (:default-team-id profile)
|
||||
:class (stl/css :team-dropdown-item)}
|
||||
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
:data-value (:default-team-id profile)
|
||||
:class (stl/css :team-dropdown-item)}
|
||||
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
|
||||
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
|
||||
(when (= (:default-team-id profile) (:id team))
|
||||
tick-icon)])
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
|
||||
(when (= (:default-team-id profile) (:id team))
|
||||
tick-icon)]
|
||||
|
||||
(for [team-item (remove :is-default (vals teams))]
|
||||
[:> dropdown-menu-item* {:on-click on-team-click
|
||||
@@ -329,19 +322,11 @@
|
||||
(when (= (:id team-item) (:id team))
|
||||
tick-icon)])
|
||||
|
||||
(when allow-create-teams
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-team-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]])
|
||||
|
||||
(when allow-create-org
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-org-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]])]))
|
||||
[:hr {:role "separator" :class (stl/css :team-separator)}]
|
||||
[:> dropdown-menu-item* {:on-click on-create-click
|
||||
:class (stl/css :team-dropdown-item :action)}
|
||||
[:span {:class (stl/css :icon-wrapper)} add-icon]
|
||||
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
|
||||
|
||||
(mf/defc team-options-dropdown*
|
||||
{::mf/private true}
|
||||
@@ -491,80 +476,9 @@
|
||||
:data-testid "delete-team"}
|
||||
(tr "dashboard.delete-team")])]))
|
||||
|
||||
|
||||
(mf/defc sidebar-org-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [teams (->> (mf/deref refs/teams)
|
||||
vals
|
||||
(group-by :organization-id)
|
||||
(map (fn [[_group entries]] (first entries)))
|
||||
vec
|
||||
(d/index-by :id))
|
||||
|
||||
teams (update-vals teams
|
||||
(fn [t]
|
||||
(assoc t :name (str "ORG: " (:organization-name t)))))
|
||||
|
||||
team (assoc team :name (str "ORG: " (:organization-name team)))
|
||||
|
||||
show-teams-menu*
|
||||
(mf/use-state false)
|
||||
|
||||
show-teams-menu?
|
||||
(deref show-teams-menu*)
|
||||
|
||||
on-show-teams-click
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(dom/stop-propagation event)
|
||||
(swap! show-teams-menu* not)))
|
||||
|
||||
on-show-teams-keydown
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(when (or (kbd/space? event)
|
||||
(kbd/enter? event))
|
||||
(dom/prevent-default event)
|
||||
(dom/stop-propagation event)
|
||||
(some-> (dom/get-current-target event)
|
||||
(dom/click!)))))
|
||||
close-teams-menu
|
||||
(mf/use-fn #(reset! show-teams-menu* false))]
|
||||
|
||||
[:div {:class (stl/css :sidebar-team-switch)}
|
||||
[:div {:class (stl/css :switch-content)}
|
||||
[:button {:class (stl/css :current-team)
|
||||
:on-click on-show-teams-click
|
||||
:on-key-down on-show-teams-keydown}
|
||||
|
||||
[:div {:class (stl/css :team-name)}
|
||||
[:img {:src (cf/resolve-team-photo-url team)
|
||||
:class (stl/css :team-picture)
|
||||
:alt (:name team)}]
|
||||
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]
|
||||
|
||||
arrow-icon]]
|
||||
|
||||
;; Teams Dropdown
|
||||
|
||||
[:> teams-selector-dropdown* {:show show-teams-menu?
|
||||
:on-close close-teams-menu
|
||||
:id "organizations-list"
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:team team
|
||||
:profile profile
|
||||
:teams teams
|
||||
:show-default-team false
|
||||
:allow-create-teams false
|
||||
:allow-create-org true}]]))
|
||||
|
||||
(mf/defc sidebar-team-switch*
|
||||
[{:keys [team profile]}]
|
||||
(let [nitrate? (contains? cf/flags :nitrate)
|
||||
org-id (when nitrate? (:organization-id team))
|
||||
teams (cond->> (mf/deref refs/teams)
|
||||
nitrate?
|
||||
(filter #(= (-> % val :organization-id) org-id)))
|
||||
(let [teams (mf/deref refs/teams)
|
||||
|
||||
subscription
|
||||
(get team :subscription)
|
||||
@@ -672,10 +586,7 @@
|
||||
:class (stl/css :dropdown :teams-dropdown)
|
||||
:team team
|
||||
:profile profile
|
||||
:teams teams
|
||||
:show-default-team true
|
||||
:allow-create-teams true
|
||||
:allow-create-org false}]
|
||||
:teams teams}]
|
||||
|
||||
[:> team-options-dropdown* {:show show-team-options-menu?
|
||||
:on-close close-team-options-menu
|
||||
@@ -792,8 +703,6 @@
|
||||
[:*
|
||||
[:div {:class (stl/css-case :sidebar-content true)
|
||||
:ref container}
|
||||
(when (contains? cf/flags :nitrate)
|
||||
[:> sidebar-org-switch* {:team team :profile profile}])
|
||||
[:> sidebar-team-switch* {:team team :profile profile}]
|
||||
|
||||
[:> sidebar-search* {:search-term search-term
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
.element-list {
|
||||
@include t.use-typography("body-large");
|
||||
color: var(--modal-text-foreground-color);
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,49 +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.layers.layer-button
|
||||
(:require-macros
|
||||
[app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema:layer-button
|
||||
[:map
|
||||
[:label :string]
|
||||
[:description {:optional true} [:maybe :string]]
|
||||
[:class {:optional true} :string]
|
||||
[:expandable {:optional true} :boolean]
|
||||
[:expanded {:optional true} :boolean]
|
||||
[:icon {:optional true} :string]
|
||||
[:on-toggle-expand fn?]])
|
||||
|
||||
(mf/defc layer-button*
|
||||
{::mf/schema schema:layer-button}
|
||||
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}]
|
||||
(let [button-props (mf/spread-props props
|
||||
{:class [class (stl/css-case :layer-button true
|
||||
:layer-button--expandable is-expandable
|
||||
:layer-button--expanded expanded)]
|
||||
:type "button"
|
||||
:on-click on-toggle-expand})]
|
||||
[:div {:class (stl/css :layer-button-wrapper)}
|
||||
[:> "button" button-props
|
||||
[:div {:class (stl/css :layer-button-content)}
|
||||
(when is-expandable
|
||||
(if expanded
|
||||
[:> icon* {:icon-id i/arrow-down :class (stl/css :folder-node-icon)}]
|
||||
[:> icon* {:icon-id i/arrow-right :class (stl/css :folder-node-icon)}]))
|
||||
(when icon
|
||||
[:> icon* {:icon-id icon :class (stl/css :layer-button-icon)}])
|
||||
[:span {:class (stl/css :layer-button-name)}
|
||||
label]
|
||||
(when description
|
||||
[:span {:class (stl/css :layer-button-description)}
|
||||
description])
|
||||
[:span {:class (stl/css :layer-button-quantity)}]]]
|
||||
[:div {:class (stl/css :layer-button-actions)}
|
||||
children]]))
|
||||
@@ -1,56 +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/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/colors.scss" as *;
|
||||
|
||||
.layer-button-wrapper {
|
||||
--layer-button-block-size: #{$sz-32};
|
||||
--layer-button-background: var(--color-background-primary);
|
||||
--layer-button-text: var(--color-foreground-secondary);
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
block-size: var(--layer-button-block-size);
|
||||
|
||||
background: var(--layer-button-background);
|
||||
color: var(--layer-button-text);
|
||||
}
|
||||
|
||||
.layer-button {
|
||||
@include use-typography("body-small");
|
||||
|
||||
appearance: none;
|
||||
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
border: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.layer-button--expanded {
|
||||
& .layer-button-name {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
}
|
||||
|
||||
.layer-button-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.layer-button-description {
|
||||
padding: var(--sp-xs);
|
||||
background-color: var(--color-background-tertiary);
|
||||
border-radius: $br-6;
|
||||
}
|
||||
@@ -223,30 +223,24 @@
|
||||
circ (* 2 Math/PI 12)
|
||||
pct (- circ (* circ (/ progress total)))
|
||||
|
||||
pwidth
|
||||
(if error?
|
||||
280
|
||||
(/ (* progress 280) total))
|
||||
pwidth (if error?
|
||||
280
|
||||
(/ (* progress 280) total))
|
||||
color (cond
|
||||
error? clr/new-danger
|
||||
healthy? (if is-default-theme?
|
||||
clr/new-primary
|
||||
clr/new-primary-light)
|
||||
(not healthy?) clr/new-warning)
|
||||
|
||||
color
|
||||
(cond
|
||||
error? clr/new-danger
|
||||
healthy? (if is-default-theme?
|
||||
clr/new-primary
|
||||
clr/new-primary-light)
|
||||
(not healthy?) clr/new-warning)
|
||||
|
||||
background-clr
|
||||
(if is-default-theme?
|
||||
clr/background-quaternary
|
||||
clr/background-quaternary-light)
|
||||
|
||||
title
|
||||
(cond
|
||||
error? (tr "workspace.options.exporting-object-error")
|
||||
complete? (tr "workspace.options.exporting-complete")
|
||||
healthy? (tr "workspace.options.exporting-object")
|
||||
(not healthy?) (tr "workspace.options.exporting-object-slow"))
|
||||
background-clr (if is-default-theme?
|
||||
clr/background-quaternary
|
||||
clr/background-quaternary-light)
|
||||
title (cond
|
||||
error? (tr "workspace.options.exporting-object-error")
|
||||
complete? (tr "workspace.options.exporting-complete")
|
||||
healthy? (tr "workspace.options.exporting-object")
|
||||
(not healthy?) (tr "workspace.options.exporting-object-slow"))
|
||||
|
||||
retry-last-export
|
||||
(mf/use-fn #(st/emit! (de/retry-last-export)))
|
||||
@@ -290,7 +284,7 @@
|
||||
:on-click retry-last-export}
|
||||
(tr "workspace.options.retry")]
|
||||
|
||||
[:span {:class (stl/css :progress)}
|
||||
[:p {:class (stl/css :progress)}
|
||||
(dm/str progress " / " total)])]
|
||||
|
||||
[:button {:class (stl/css :progress-close-button)
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.flex-controls.gap
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.geom.shapes :as gsh]
|
||||
@@ -17,8 +16,6 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -30,11 +27,10 @@
|
||||
(mf/defc gap-display
|
||||
[{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value
|
||||
on-move-selected on-context-menu on-change]}]
|
||||
on-move-selected on-context-menu]}]
|
||||
(let [resizing (mf/use-var nil)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (:resize-negate? rect-data)
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -47,55 +43,32 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-gap (assoc gap gap-type val)]
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[frame-id]
|
||||
(ctm/change-property (ctm/empty) :layout-gap layout-gap))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (and (features/active-feature? @st/state "render-wasm/v1") (= @resizing gap-type))
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing nil)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps frame-id gap-type gap)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! last-pos pos)
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(when (= @resizing gap-type)
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-gap (assoc gap gap-type val)
|
||||
modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))]
|
||||
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:g.gap-rect
|
||||
[:rect.info-area
|
||||
@@ -147,17 +120,10 @@
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
workspace-modifiers (mf/deref refs/workspace-modifiers)
|
||||
workspace-wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
|
||||
|
||||
gap-selected (mf/deref refs/workspace-gap-selected)
|
||||
hover (mf/use-state nil)
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
frame
|
||||
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
|
||||
|
||||
padding (:layout-padding frame)
|
||||
gap (:layout-gap frame)
|
||||
{:keys [width height x1 y1]} (:selrect frame)
|
||||
@@ -166,12 +132,6 @@
|
||||
(reset! hover-value val))
|
||||
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
negate {:column-gap (if flip-x true false)
|
||||
:row-gap (if flip-y true false)}
|
||||
|
||||
@@ -183,16 +143,8 @@
|
||||
(= :column-reverse saved-dir))
|
||||
(drop-last children)
|
||||
(rest children))
|
||||
children-to-display
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [modifiers (into {} workspace-wasm-modifiers)]
|
||||
(->> children-to-display
|
||||
;;(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))
|
||||
(map (fn [shape]
|
||||
(gsh/apply-transform shape (get modifiers (:id shape)))))))
|
||||
|
||||
(->> children-to-display
|
||||
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))))
|
||||
children-to-display (->> children-to-display
|
||||
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers]))))
|
||||
|
||||
wrap-blocks
|
||||
(let [block-children (->> children
|
||||
@@ -320,22 +272,20 @@
|
||||
[:g.gaps {:pointer-events "visible"}
|
||||
(for [[index display-item] (d/enumerate (concat display-blocks display-children))]
|
||||
(let [gap-type (:gap-type display-item)]
|
||||
[:& gap-display
|
||||
{:key (str frame-id index)
|
||||
:frame-id frame-id
|
||||
:zoom zoom
|
||||
:gap-type gap-type
|
||||
:gap gap
|
||||
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:on-change on-change
|
||||
:rect-data display-item
|
||||
:hover? (= @hover gap-type)
|
||||
:selected? (= gap-selected gap-type)
|
||||
:mouse-pos mouse-pos
|
||||
:hover-value hover-value}]))
|
||||
[:& gap-display {:key (str frame-id index)
|
||||
:frame-id frame-id
|
||||
:zoom zoom
|
||||
:gap-type gap-type
|
||||
:gap gap
|
||||
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:rect-data display-item
|
||||
:hover? (= @hover gap-type)
|
||||
:selected? (= gap-selected gap-type)
|
||||
:mouse-pos mouse-pos
|
||||
:hover-value hover-value}]))
|
||||
|
||||
(when @hover
|
||||
[:& fcc/flex-display-pill
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
|
||||
(ns app.main.ui.flex-controls.margin
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -20,14 +17,11 @@
|
||||
[app.util.dom :as dom]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc margin-display
|
||||
[{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin
|
||||
on-pointer-enter on-pointer-leave on-change
|
||||
rect-data hover? selected? mouse-pos hover-value]}]
|
||||
(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value]}]
|
||||
(let [resizing? (mf/use-var false)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (true? (:resize-negate? rect-data))
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -40,69 +34,39 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-item-margin
|
||||
(cond
|
||||
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
|
||||
hover-v? (assoc margin :m1 val :m3 val)
|
||||
hover-h? (assoc margin :m2 val :m4 val)
|
||||
:else (assoc margin margin-num val))
|
||||
|
||||
layout-item-margin-type
|
||||
(if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)]
|
||||
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[shape-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-item-margin layout-item-margin)
|
||||
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps shape-id margin-num margin)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing? false)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(reset! last-pos pos)
|
||||
(when @resizing?
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-item-margin (cond
|
||||
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
|
||||
hover-v? (assoc margin :m1 val :m3 val)
|
||||
hover-h? (assoc margin :m2 val :m4 val)
|
||||
:else (assoc margin margin-num val))
|
||||
layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)
|
||||
modifiers (dwm/create-modif-tree [shape-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-item-margin layout-item-margin)
|
||||
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))]
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:rect.margin-rect
|
||||
{:x (:x rect-data)
|
||||
@@ -125,11 +89,6 @@
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
margins-selected (mf/deref refs/workspace-margins-selected)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
shape
|
||||
(ctm/apply-structure-modifiers shape (dm/get-in @current-modifiers [shape-id :modifiers]))
|
||||
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
@@ -138,67 +97,50 @@
|
||||
hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?)
|
||||
margin (:layout-item-margin shape)
|
||||
{:keys [width height x1 x2 y1 y2]} (:selrect shape)
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val)))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! hover nil)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
hover?
|
||||
(fn [value]
|
||||
(or hover-all?
|
||||
(and (or (= value :m1) (= value :m3)) hover-v?)
|
||||
(and (or (= value :m2) (= value :m4)) hover-h?)
|
||||
(= @hover value)))
|
||||
|
||||
margin-display-data
|
||||
{:m1 {:key (str shape-id "-m1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
|
||||
:width width
|
||||
:height (:m1 margin)
|
||||
:initial-value (:m1 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m2 {:key (str shape-id "-m2")
|
||||
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
|
||||
:y y1
|
||||
:width (:m2 margin)
|
||||
:height height
|
||||
:initial-value (:m2 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}
|
||||
:m3 {:key (str shape-id "-m3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
|
||||
:width width
|
||||
:height (:m3 margin)
|
||||
:initial-value (:m3 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m4 {:key (str shape-id "-m4")
|
||||
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
|
||||
:y y1
|
||||
:width (:m4 margin)
|
||||
:height height
|
||||
:initial-value (:m4 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}}]
|
||||
on-pointer-enter (fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val))
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
hover? #(or hover-all?
|
||||
(and (or (= % :m1) (= % :m3)) hover-v?)
|
||||
(and (or (= % :m2) (= % :m4)) hover-h?)
|
||||
(= @hover %))
|
||||
margin-display-data {:m1 {:key (str shape-id "-m1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
|
||||
:width width
|
||||
:height (:m1 margin)
|
||||
:initial-value (:m1 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m2 {:key (str shape-id "-m2")
|
||||
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
|
||||
:y y1
|
||||
:width (:m2 margin)
|
||||
:height height
|
||||
:initial-value (:m2 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}
|
||||
:m3 {:key (str shape-id "-m3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
|
||||
:width width
|
||||
:height (:m3 margin)
|
||||
:initial-value (:m3 margin)
|
||||
:resize-type :top
|
||||
:resize-axis :y
|
||||
:resize-negate? (:flip-y frame)}
|
||||
:m4 {:key (str shape-id "-m4")
|
||||
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
|
||||
:y y1
|
||||
:width (:m4 margin)
|
||||
:height height
|
||||
:initial-value (:m4 margin)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:flip-x frame)}}]
|
||||
|
||||
[:g.margins {:pointer-events "visible"}
|
||||
(for [[margin-num rect-data] margin-display-data]
|
||||
@@ -213,7 +155,6 @@
|
||||
:margin margin
|
||||
:on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num))
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-change on-change
|
||||
:rect-data rect-data
|
||||
:hover? (hover? margin-num)
|
||||
:selected? (get margins-selected margin-num)
|
||||
|
||||
@@ -6,12 +6,9 @@
|
||||
|
||||
(ns app.main.ui.flex-controls.padding
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.modifiers :as ctm]
|
||||
[app.main.data.workspace.modifiers :as dwm]
|
||||
[app.main.data.workspace.transforms :as dwt]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.css-cursors :as cur]
|
||||
@@ -21,13 +18,11 @@
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc padding-display
|
||||
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter
|
||||
on-pointer-leave rect-data hover? selected? mouse-pos hover-value on-move-selected
|
||||
on-context-menu on-change]}]
|
||||
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave
|
||||
rect-data hover? selected? mouse-pos hover-value on-move-selected on-context-menu]}]
|
||||
(let [resizing? (mf/use-var false)
|
||||
start (mf/use-var nil)
|
||||
original-value (mf/use-var 0)
|
||||
last-pos (mf/use-var nil)
|
||||
negate? (true? (:resize-negate? rect-data))
|
||||
axis (:resize-axis rect-data)
|
||||
|
||||
@@ -40,69 +35,41 @@
|
||||
(reset! start (dom/get-client-position event))
|
||||
(reset! original-value (:initial-value rect-data))))
|
||||
|
||||
calc-modifiers
|
||||
(mf/use-fn
|
||||
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
|
||||
(fn [pos]
|
||||
(let [delta
|
||||
(-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
|
||||
val
|
||||
(int (max (+ @original-value (/ delta zoom)) 0))
|
||||
|
||||
layout-padding
|
||||
(cond
|
||||
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
|
||||
hover-v? (assoc padding :p1 val :p3 val)
|
||||
hover-h? (assoc padding :p2 val :p4 val)
|
||||
:else (assoc padding padding-num val))
|
||||
|
||||
|
||||
layout-padding-type
|
||||
(if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)]
|
||||
[val
|
||||
(dwm/create-modif-tree
|
||||
[frame-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-padding layout-padding)
|
||||
(ctm/change-property :layout-padding-type layout-padding-type)))])))
|
||||
|
||||
on-lost-pointer-capture
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers)
|
||||
(mf/deps frame-id padding-num padding)
|
||||
(fn [event]
|
||||
(dom/release-pointer event)
|
||||
|
||||
(when (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [[_ modifiers] (calc-modifiers @last-pos)]
|
||||
(st/emit! (dwm/apply-wasm-modifiers modifiers)
|
||||
(dwt/finish-transform))))
|
||||
|
||||
(reset! resizing? false)
|
||||
(reset! start nil)
|
||||
(reset! original-value 0)
|
||||
|
||||
(when (not (features/active-feature? @st/state "render-wasm/v1"))
|
||||
(st/emit! (dwm/apply-modifiers)))))
|
||||
(st/emit! (dwm/apply-modifiers))))
|
||||
|
||||
on-pointer-move
|
||||
(mf/use-fn
|
||||
(mf/deps calc-modifiers on-change)
|
||||
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
|
||||
(fn [event]
|
||||
(let [pos (dom/get-client-position event)]
|
||||
(reset! mouse-pos (point->viewport pos))
|
||||
(reset! last-pos pos)
|
||||
(when @resizing?
|
||||
(let [[val modifiers] (calc-modifiers pos)]
|
||||
(reset! hover-value val)
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(st/emit! (dwm/set-wasm-modifiers modifiers))
|
||||
(st/emit! (dwm/set-modifiers modifiers)))
|
||||
(let [delta (-> (gpt/to-vec @start pos)
|
||||
(cond-> negate? gpt/negate)
|
||||
(get axis))
|
||||
val (int (max (+ @original-value (/ delta zoom)) 0))
|
||||
layout-padding (cond
|
||||
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
|
||||
hover-v? (assoc padding :p1 val :p3 val)
|
||||
hover-h? (assoc padding :p2 val :p4 val)
|
||||
:else (assoc padding padding-num val))
|
||||
|
||||
(when on-change
|
||||
(on-change modifiers)))))))]
|
||||
|
||||
layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)
|
||||
modifiers (dwm/create-modif-tree [frame-id]
|
||||
(-> (ctm/empty)
|
||||
(ctm/change-property :layout-padding layout-padding)
|
||||
(ctm/change-property :layout-padding-type layout-padding-type)))]
|
||||
(reset! hover-value val)
|
||||
(st/emit! (dwm/set-modifiers modifiers)))))))]
|
||||
|
||||
[:g.padding-rect
|
||||
[:rect.info-area
|
||||
@@ -138,108 +105,77 @@
|
||||
:on-lost-pointer-capture on-lost-pointer-capture
|
||||
:on-pointer-move on-pointer-move
|
||||
:on-context-menu on-context-menu
|
||||
:class
|
||||
(when (or hover? selected?)
|
||||
(if (= (:resize-axis rect-data) :x)
|
||||
(cur/get-dynamic "resize-ew" 0)
|
||||
(cur/get-dynamic "resize-ew" 90)))
|
||||
|
||||
:style
|
||||
{:fill (if (or hover? selected?) fcc/distance-color "none")
|
||||
:opacity (if selected? 0 1)}}])]))
|
||||
:class (when (or hover? selected?)
|
||||
(if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90)))
|
||||
:style {:fill (if (or hover? selected?) fcc/distance-color "none")
|
||||
:opacity (if selected? 0 1)}}])]))
|
||||
|
||||
(mf/defc padding-rects
|
||||
[{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}]
|
||||
(let [frame-id (:id frame)
|
||||
paddings-selected (mf/deref refs/workspace-paddings-selected)
|
||||
current-modifiers (mf/use-state nil)
|
||||
|
||||
frame
|
||||
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
|
||||
|
||||
hover-value (mf/use-state 0)
|
||||
mouse-pos (mf/use-state nil)
|
||||
hover (mf/use-state nil)
|
||||
|
||||
hover-all? (and (not (nil? @hover)) alt?)
|
||||
hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?)
|
||||
hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?)
|
||||
padding (:layout-padding frame)
|
||||
{:keys [width height x1 x2 y1 y2]} (:selrect frame)
|
||||
on-pointer-enter (fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val))
|
||||
on-pointer-leave #(reset! hover nil)
|
||||
pill-width (/ fcc/flex-display-pill-width zoom)
|
||||
pill-height (/ fcc/flex-display-pill-height zoom)
|
||||
hover? #(or hover-all?
|
||||
(and (or (= % :p1) (= % :p3)) hover-v?)
|
||||
(and (or (= % :p2) (= % :p4)) hover-h?)
|
||||
(= @hover %))
|
||||
negate {:p1 (if (:flip-y frame) true false)
|
||||
:p2 (if (:flip-x frame) true false)
|
||||
:p3 (if (:flip-y frame) true false)
|
||||
:p4 (if (:flip-x frame) true false)}
|
||||
negate (cond-> negate
|
||||
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
|
||||
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
|
||||
|
||||
negate
|
||||
{:p1 (if (:flip-y frame) true false)
|
||||
:p2 (if (:flip-x frame) true false)
|
||||
:p3 (if (:flip-y frame) true false)
|
||||
:p4 (if (:flip-x frame) true false)}
|
||||
|
||||
negate
|
||||
(cond-> negate
|
||||
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
|
||||
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
|
||||
|
||||
padding-rect-data
|
||||
{:p1 {:key (str frame-id "-p1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
|
||||
:width width
|
||||
:height (:p1 padding)
|
||||
:initial-value (:p1 padding)
|
||||
:resize-type (if (:flip-y frame) :bottom :top)
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p1 negate)}
|
||||
:p2 {:key (str frame-id "-p2")
|
||||
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
|
||||
:y y1
|
||||
:width (:p2 padding)
|
||||
:height height
|
||||
:initial-value (:p2 padding)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p2 negate)}
|
||||
:p3 {:key (str frame-id "-p3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
|
||||
:width width
|
||||
:height (:p3 padding)
|
||||
:initial-value (:p3 padding)
|
||||
:resize-type :bottom
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p3 negate)}
|
||||
:p4 {:key (str frame-id "-p4")
|
||||
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
|
||||
:y y1
|
||||
:width (:p4 padding)
|
||||
:height height
|
||||
:initial-value (:p4 padding)
|
||||
:resize-type (if (:flip-x frame) :right :left)
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p4 negate)}}
|
||||
|
||||
on-pointer-enter
|
||||
(mf/use-fn
|
||||
(fn [hover-type val]
|
||||
(reset! hover hover-type)
|
||||
(reset! hover-value val)))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(fn []
|
||||
(reset! hover nil)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(fn [modifiers]
|
||||
(reset! current-modifiers modifiers)))
|
||||
|
||||
hover?
|
||||
(fn [value]
|
||||
(or hover-all?
|
||||
(and (or (= value :p1) (= value :p3)) hover-v?)
|
||||
(and (or (= value :p2) (= value :p4)) hover-h?)
|
||||
(= @hover value)))]
|
||||
padding-rect-data {:p1 {:key (str frame-id "-p1")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
|
||||
:width width
|
||||
:height (:p1 padding)
|
||||
:initial-value (:p1 padding)
|
||||
:resize-type (if (:flip-y frame) :bottom :top)
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p1 negate)}
|
||||
:p2 {:key (str frame-id "-p2")
|
||||
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
|
||||
:y y1
|
||||
:width (:p2 padding)
|
||||
:height height
|
||||
:initial-value (:p2 padding)
|
||||
:resize-type :left
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p2 negate)}
|
||||
:p3 {:key (str frame-id "-p3")
|
||||
:x x1
|
||||
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
|
||||
:width width
|
||||
:height (:p3 padding)
|
||||
:initial-value (:p3 padding)
|
||||
:resize-type :bottom
|
||||
:resize-axis :y
|
||||
:resize-negate? (:p3 negate)}
|
||||
:p4 {:key (str frame-id "-p4")
|
||||
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
|
||||
:y y1
|
||||
:width (:p4 padding)
|
||||
:height height
|
||||
:initial-value (:p4 padding)
|
||||
:resize-type (if (:flip-x frame) :right :left)
|
||||
:resize-axis :x
|
||||
:resize-negate? (:p4 negate)}}]
|
||||
|
||||
[:g.paddings {:pointer-events "visible"}
|
||||
(for [[padding-num rect-data] padding-rect-data]
|
||||
@@ -258,11 +194,9 @@
|
||||
:on-pointer-leave on-pointer-leave
|
||||
:on-move-selected on-move-selected
|
||||
:on-context-menu on-context-menu
|
||||
:on-change on-change
|
||||
:hover? (hover? padding-num)
|
||||
:selected? (get paddings-selected padding-num)
|
||||
:rect-data rect-data}])
|
||||
|
||||
(when @hover
|
||||
[:& fcc/flex-display-pill
|
||||
{:height pill-height
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
[:button {:class (stl/css :cta-button :bottom-link)
|
||||
:on-click cta-link-trial} cta-text-trial])])
|
||||
|
||||
(defn- make-management-form-schema [min-editors]
|
||||
(defn schema:seats-form [min-editors]
|
||||
[:map {:title "SeatsForm"}
|
||||
[:min-members [::sm/number {:min min-editors
|
||||
:max 9999}]]
|
||||
@@ -87,6 +87,7 @@
|
||||
{::mf/register modal/components
|
||||
::mf/register-as :management-dialog}
|
||||
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
|
||||
|
||||
(let [unlimited-modal-step*
|
||||
(mf/use-state 1)
|
||||
|
||||
@@ -111,12 +112,9 @@
|
||||
{:min-members min-editors
|
||||
:redirect-to-payment-details false})
|
||||
|
||||
schema
|
||||
(mf/with-memo [min-editors]
|
||||
(make-management-form-schema min-editors))
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema :initial initial)
|
||||
(fm/use-form :schema (schema:seats-form min-editors)
|
||||
:initial initial)
|
||||
|
||||
submit-in-progress
|
||||
(mf/use-ref false)
|
||||
@@ -336,15 +334,11 @@
|
||||
[:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]]
|
||||
|
||||
[:div {:class (stl/css :modal-end)}
|
||||
[:div {:class (stl/css :modal-title)}
|
||||
(tr "subscription.settings.sucess.dialog.title" subscription-name)]
|
||||
[:div {:class (stl/css :modal-title)} (tr "subscription.settings.sucess.dialog.title" subscription-name)]
|
||||
(when (not= subscription-name "professional")
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
(tr "subscription.settings.success.dialog.thanks" subscription-name)])
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
(tr "subscription.settings.success.dialog.description")]
|
||||
[:p {:class (stl/css :modal-text-large)}
|
||||
(tr "subscription.settings.sucess.dialog.footer")]
|
||||
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.thanks" subscription-name)])
|
||||
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")]
|
||||
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.sucess.dialog.footer")]
|
||||
|
||||
[:div {:class (stl/css :success-action-buttons)}
|
||||
[:input
|
||||
@@ -424,11 +418,7 @@
|
||||
(mf/with-effect []
|
||||
(dom/set-html-title (tr "subscription.labels")))
|
||||
|
||||
(mf/with-effect [authenticated?
|
||||
show-subscription-success-modal?
|
||||
show-trial-subscription-modal?
|
||||
success-modal-is-trial?
|
||||
subscription]
|
||||
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? success-modal-is-trial? subscription]
|
||||
(when ^boolean authenticated?
|
||||
(cond
|
||||
^boolean show-trial-subscription-modal?
|
||||
|
||||
@@ -28,7 +28,6 @@
|
||||
{::mf/wrap-props false}
|
||||
[props]
|
||||
(let [{:keys [position-data content] :as shape} (obj/get props "shape")
|
||||
is-render? (mf/use-ctx ctx/is-render?)
|
||||
is-component? (mf/use-ctx ctx/is-component?)]
|
||||
|
||||
(mf/with-memo [content]
|
||||
@@ -42,5 +41,5 @@
|
||||
;; Only use this for component preview, otherwise the dashboard thumbnails
|
||||
;; will give a tainted canvas error because the `foreignObject` cannot be
|
||||
;; rendered.
|
||||
(and (nil? position-data) (or is-component? is-render?))
|
||||
(and (nil? position-data) is-component?)
|
||||
[:> fo/text-shape props])))
|
||||
|
||||
@@ -12,20 +12,18 @@
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc text-edition-outline
|
||||
[{:keys [shape zoom modifiers]}]
|
||||
(if (features/active-feature? @st/state "render-wasm/v1")
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions (:id shape))
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)]
|
||||
(let [selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[{:keys [x y width height]} transform] (dsh/get-selrect selrect-transform shape)]
|
||||
[:rect.main.viewport-selrect
|
||||
{:x (:x selrect)
|
||||
:y (:y selrect)
|
||||
:width (max width (:width selrect))
|
||||
:height (max height (:height selrect))
|
||||
{:x x
|
||||
:y y
|
||||
:width width
|
||||
:height height
|
||||
:transform transform
|
||||
:style {:stroke "var(--color-accent-tertiary)"
|
||||
:stroke-width (/ 1 zoom)
|
||||
|
||||
@@ -320,12 +320,10 @@
|
||||
|
||||
[{:keys [x y width height]} transform]
|
||||
(if render-wasm?
|
||||
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
|
||||
(let [{:keys [height]} (wasm.api/get-text-dimensions shape-id)
|
||||
selrect-transform (mf/deref refs/workspace-selrect)
|
||||
[selrect transform] (dsh/get-selrect selrect-transform shape)
|
||||
selrect-height (:height selrect)
|
||||
selrect-width (:width selrect)
|
||||
max-width (max width selrect-width)
|
||||
max-height (max height selrect-height)
|
||||
valign (-> shape :content :vertical-align)
|
||||
y (:y selrect)
|
||||
@@ -333,9 +331,9 @@
|
||||
(case valign
|
||||
"bottom" (- y (- height selrect-height))
|
||||
"center" (- y (/ (- height selrect-height) 2))
|
||||
y)
|
||||
"top" y)
|
||||
y)]
|
||||
[(assoc selrect :y y :width max-width :height max-height) transform])
|
||||
[(assoc selrect :y y :width (:width selrect) :height max-height) transform])
|
||||
|
||||
(let [bounds (gst/shape->rect shape)
|
||||
x (mth/min (dm/get-prop bounds :x)
|
||||
@@ -354,7 +352,7 @@
|
||||
(obj/merge!
|
||||
#js {"--editor-container-width" (dm/str width "px")
|
||||
"--editor-container-height" (dm/str height "px")
|
||||
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")})
|
||||
"--fallback-families" (dm/str (str/join ", " fallback-families))})
|
||||
|
||||
(not render-wasm?)
|
||||
(obj/merge!
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.layer-row {
|
||||
--layer-indentation-size: calc(#{deprecated.$s-4} * 6);
|
||||
@@ -88,7 +87,7 @@
|
||||
height: deprecated.$s-32;
|
||||
width: calc(100% - (var(--depth) * var(--layer-indentation-size)));
|
||||
cursor: pointer;
|
||||
min-width: px2rem(140);
|
||||
|
||||
&.filtered {
|
||||
width: calc(100% - deprecated.$s-12);
|
||||
}
|
||||
|
||||
@@ -211,7 +211,9 @@
|
||||
overflow-x: auto;
|
||||
overflow-y: overlay;
|
||||
scrollbar-gutter: stable;
|
||||
}
|
||||
.element-list {
|
||||
display: grid;
|
||||
|
||||
.element-list {
|
||||
width: var(--left-sidebar-width);
|
||||
display: grid;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:placeholder (cond
|
||||
(not all-equal?)
|
||||
(tr "settings.multiple")
|
||||
"Mixed"
|
||||
(= :multiple (:r1 values))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]]
|
||||
[app.util.dom :as dom]
|
||||
@@ -235,20 +234,20 @@
|
||||
|
||||
[:& radio-button
|
||||
{:value "fix"
|
||||
:icon i/fixed-width
|
||||
:icon deprecated-icon/fixed-width
|
||||
:title "Fix width"
|
||||
:id "behaviour-h-fix"}]
|
||||
|
||||
(when has-fill
|
||||
[:& radio-button
|
||||
{:value "fill"
|
||||
:icon i/fill-content
|
||||
:icon deprecated-icon/fill-content
|
||||
:title "Width 100%"
|
||||
:id "behaviour-h-fill"}])
|
||||
(when is-auto
|
||||
[:& radio-button
|
||||
{:value "auto"
|
||||
:icon i/hug-content
|
||||
:icon deprecated-icon/hug-content
|
||||
:title "Fit content (Horizontal)"
|
||||
:id "behaviour-h-auto"}])]])
|
||||
|
||||
@@ -269,7 +268,7 @@
|
||||
|
||||
[:& radio-button
|
||||
{:value "fix"
|
||||
:icon i/fixed-width
|
||||
:icon deprecated-icon/fixed-width
|
||||
:icon-class (stl/css :rotated)
|
||||
:title "Fix height"
|
||||
:id "behaviour-v-fix"}]
|
||||
@@ -277,14 +276,14 @@
|
||||
(when has-fill
|
||||
[:& radio-button
|
||||
{:value "fill"
|
||||
:icon i/fill-content
|
||||
:icon deprecated-icon/fill-content
|
||||
:icon-class (stl/css :rotated)
|
||||
:title "Height 100%"
|
||||
:id "behaviour-v-fill"}])
|
||||
(when is-auto
|
||||
[:& radio-button
|
||||
{:value "auto"
|
||||
:icon i/hug-content
|
||||
:icon deprecated-icon/hug-content
|
||||
:icon-class (stl/css :rotated)
|
||||
:title "Fit content (Vertical)"
|
||||
:id "behaviour-v-auto"}])]])
|
||||
|
||||
@@ -265,13 +265,11 @@
|
||||
(mf/deps font on-change)
|
||||
(fn [new-variant-id]
|
||||
(let [variant (d/seek #(= new-variant-id (:id %)) (:variants font))]
|
||||
(when-not (nil? variant)
|
||||
(on-change {:font-id (:id font)
|
||||
:font-family (:family font)
|
||||
:font-variant-id new-variant-id
|
||||
:font-weight (:weight variant)
|
||||
:font-style (:style variant)}))
|
||||
|
||||
(on-change {:font-id (:id font)
|
||||
:font-family (:family font)
|
||||
:font-variant-id new-variant-id
|
||||
:font-weight (:weight variant)
|
||||
:font-style (:style variant)})
|
||||
(dom/blur! (dom/get-target new-variant-id)))))
|
||||
|
||||
on-font-select
|
||||
@@ -344,13 +342,12 @@
|
||||
{:value (:id variant)
|
||||
:key (pr-str variant)
|
||||
:label (:name variant)})))
|
||||
variant-options (if (= font-variant-id :multiple)
|
||||
variant-options (if (= font-size :multiple)
|
||||
(conj basic-variant-options
|
||||
{:value ""
|
||||
{:value :multiple
|
||||
:key :multiple-variants
|
||||
:label "--"})
|
||||
basic-variant-options)]
|
||||
|
||||
;; TODO Add disabled mode
|
||||
[:& select
|
||||
{:class (stl/css :font-variant-select)
|
||||
@@ -382,7 +379,6 @@
|
||||
:step 0.1
|
||||
:default-value "1.2"
|
||||
:class (stl/css :line-height-input)
|
||||
:aria-label (tr "inspect.attributes.typography.line-height")
|
||||
:value (attr->string line-height)
|
||||
:placeholder (if (= :multiple line-height) (tr "settings.multiple") "--")
|
||||
:nillable (= :multiple line-height)
|
||||
@@ -401,7 +397,6 @@
|
||||
:step 0.1
|
||||
:default-value "0"
|
||||
:class (stl/css :letter-spacing-input)
|
||||
:aria-label (tr "inspect.attributes.typography.letter-spacing")
|
||||
:value (attr->string letter-spacing)
|
||||
:placeholder (if (= :multiple letter-spacing) (tr "settings.multiple") "--")
|
||||
:on-change #(handle-change % :letter-spacing)
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
(mf/defc color-token-row*
|
||||
{::mf/private true}
|
||||
[{:keys [active-tokens applied-token-name color on-swatch-click-token detach-token open-modal-from-token]}]
|
||||
[{:keys [active-tokens color-token color on-swatch-click-token detach-token open-modal-from-token]}]
|
||||
(let [;; `active-tokens` may be provided as a `delay` (lazy computation).
|
||||
;; In that case we must deref it (`@active-tokens`) to force evaluation
|
||||
;; and obtain the actual value. If it’s already realized (not a delay),
|
||||
@@ -77,22 +77,21 @@
|
||||
@active-tokens
|
||||
active-tokens)
|
||||
|
||||
active-color-tokens (:color active-tokens)
|
||||
color-tokens (:color active-tokens)
|
||||
|
||||
token (some #(when (= (:name %) applied-token-name) %) active-color-tokens)
|
||||
token (some #(when (= (:name %) color-token) %) color-tokens)
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps detach-token token applied-token-name)
|
||||
(mf/deps detach-token token color-token)
|
||||
(fn []
|
||||
(let [token (or token applied-token-name)]
|
||||
(let [token (or token color-token)]
|
||||
(detach-token token))))
|
||||
|
||||
has-errors (some? (:errors token))
|
||||
token-name (:name token)
|
||||
resolved (:resolved-value token)
|
||||
not-active (and (empty? active-tokens)
|
||||
(nil? token))
|
||||
not-active (and (some? active-tokens) (nil? token))
|
||||
id (dm/str (:id token) "-name")
|
||||
swatch-tooltip-content (cond
|
||||
not-active
|
||||
@@ -110,7 +109,7 @@
|
||||
#(mf/html
|
||||
[:div
|
||||
[:span (dm/str (tr "workspace.tokens.token-name") ": ")]
|
||||
[:span {:class (stl/css :token-name-tooltip)} applied-token-name]]))]
|
||||
[:span {:class (stl/css :token-name-tooltip)} color-token]]))]
|
||||
|
||||
[:div {:class (stl/css :color-info)}
|
||||
[:div {:class (stl/css-case :token-color-wrapper true
|
||||
@@ -129,7 +128,7 @@
|
||||
:class (stl/css :token-tooltip)}
|
||||
[:div {:class (stl/css :token-name)
|
||||
:aria-labelledby id}
|
||||
(or token-name applied-token-name)]]
|
||||
(or token-name color-token)]]
|
||||
[:div {:class (stl/css :token-actions)}
|
||||
[:> icon-button*
|
||||
{:variant "action"
|
||||
@@ -147,11 +146,7 @@
|
||||
on-change on-reorder on-detach on-open on-close on-remove origin on-detach-token
|
||||
disable-drag on-focus on-blur select-only select-on-focus on-token-change applied-token]}]
|
||||
|
||||
(let [;; TODO: Remove this workaround fixing `get-attrs*` fn on sidebar/options/shapes/multiple.cljs
|
||||
applied-token (if (= :multiple applied-token)
|
||||
nil
|
||||
applied-token)
|
||||
token-color (contains? cfg/flags :token-color)
|
||||
(let [token-color (contains? cfg/flags :token-color)
|
||||
libraries (mf/deref refs/files)
|
||||
|
||||
color-without-hash (mf/use-memo
|
||||
@@ -182,6 +177,7 @@
|
||||
(-> (deref active-tokens*)
|
||||
(select-keys (get tk/tokens-by-input origin))
|
||||
(not-empty)))))
|
||||
|
||||
on-focus'
|
||||
(mf/use-fn
|
||||
(mf/deps on-focus)
|
||||
@@ -356,7 +352,7 @@
|
||||
(cond
|
||||
(and token-color applied-token)
|
||||
[:> color-token-row* {:active-tokens tokens
|
||||
:applied-token-name applied-token
|
||||
:color-token applied-token
|
||||
:color (dissoc color :ref-id :ref-file)
|
||||
:on-swatch-click-token on-swatch-click-token
|
||||
:detach-token detach-token
|
||||
|
||||
@@ -63,8 +63,7 @@
|
||||
:data {:index index})
|
||||
[nil nil])
|
||||
|
||||
stroke-color-token
|
||||
(:stroke-color applied-tokens)
|
||||
stroke-color-token (:stroke-color applied-tokens)
|
||||
|
||||
on-color-change-refactor
|
||||
(mf/use-fn
|
||||
|
||||
@@ -268,8 +268,8 @@
|
||||
:on-click modal/hide!}
|
||||
(tr "labels.cancel")]
|
||||
[:> import-type-dropdown*
|
||||
{:options [{:label (tr "workspace.tokens.import-menu-json-option") :value :file}
|
||||
{:label (tr "workspace.tokens.import-menu-zip-option") :value :zip}
|
||||
{:options [{:label (tr "workspace.tokens.import-menu-zip-option") :value :zip}
|
||||
{:label (tr "workspace.tokens.import-menu-json-option") :value :file}
|
||||
{:label (tr "workspace.tokens.import-menu-folder-option") :value :folder}]
|
||||
:on-click handle-import-action
|
||||
:text-render render-button-text
|
||||
|
||||
@@ -44,39 +44,6 @@
|
||||
[(seq (array/sort! empty))
|
||||
(seq (array/sort! filled))]))))
|
||||
|
||||
(mf/defc selected-set-info*
|
||||
{::mf/private true}
|
||||
[{:keys [tokens-lib selected-token-set-id]}]
|
||||
(let [selected-token-set
|
||||
(mf/with-memo [tokens-lib]
|
||||
(when selected-token-set-id
|
||||
(some-> tokens-lib (ctob/get-set selected-token-set-id))))
|
||||
|
||||
active-token-sets-names
|
||||
(mf/with-memo [tokens-lib]
|
||||
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
||||
|
||||
token-set-active?
|
||||
(mf/use-fn
|
||||
(mf/deps active-token-sets-names)
|
||||
(fn [name]
|
||||
(contains? active-token-sets-names name)))]
|
||||
[:div {:class (stl/css :sets-header-container)}
|
||||
[:> text* {:as "span"
|
||||
:typography "headline-small"
|
||||
:class (stl/css :sets-header)}
|
||||
(tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
;; NOTE: when no set in tokens-lib, the selected-token-set-id
|
||||
;; will be `nil`, so for properly hide the inactive message we
|
||||
;; check that at least `selected-token-set-id` has a value
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
[:*
|
||||
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
|
||||
(tr "workspace.tokens.inactive-set")]])]]))
|
||||
|
||||
(mf/defc tokens-section*
|
||||
{::mf/private true}
|
||||
[{:keys [tokens-lib active-tokens resolved-active-tokens]}]
|
||||
@@ -98,7 +65,9 @@
|
||||
selected-token-set-id
|
||||
(mf/deref refs/selected-token-set-id)
|
||||
|
||||
|
||||
selected-token-set
|
||||
(when selected-token-set-id
|
||||
(some-> tokens-lib (ctob/get-set selected-token-set-id)))
|
||||
|
||||
;; If we have not selected any set explicitly we just
|
||||
;; select the first one from the list of sets
|
||||
@@ -123,9 +92,15 @@
|
||||
tokens)]
|
||||
(ctob/group-by-type tokens)))
|
||||
|
||||
active-token-sets-names
|
||||
(mf/with-memo [tokens-lib]
|
||||
(some-> tokens-lib (ctob/get-active-themes-set-names)))
|
||||
|
||||
|
||||
|
||||
token-set-active?
|
||||
(mf/use-fn
|
||||
(mf/deps active-token-sets-names)
|
||||
(fn [name]
|
||||
(contains? active-token-sets-names name)))
|
||||
|
||||
[empty-group filled-group]
|
||||
(mf/with-memo [tokens-by-type]
|
||||
@@ -143,27 +118,34 @@
|
||||
|
||||
[:*
|
||||
[:& token-context-menu]
|
||||
|
||||
[:& selected-set-info* {:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]
|
||||
[:div {:class (stl/css :sets-header-container)}
|
||||
[:> text* {:as "span" :typography "headline-small" :class (stl/css :sets-header)} (tr "workspace.tokens.tokens-section-title" (ctob/get-name selected-token-set))]
|
||||
[:div {:class (stl/css :sets-header-status) :title (tr "workspace.tokens.inactive-set-description")}
|
||||
;; NOTE: when no set in tokens-lib, the selected-token-set-id
|
||||
;; will be `nil`, so for properly hide the inactive message we
|
||||
;; check that at least `selected-token-set-id` has a value
|
||||
(when (and (some? selected-token-set-id)
|
||||
(not (token-set-active? (ctob/get-name selected-token-set))))
|
||||
[:*
|
||||
[:> icon* {:class (stl/css :sets-header-status-icon) :icon-id i/eye-off}]
|
||||
[:> text* {:as "span" :typography "body-small" :class (stl/css :sets-header-status-text)}
|
||||
(tr "workspace.tokens.inactive-set")]])]]
|
||||
|
||||
(for [type filled-group]
|
||||
(let [tokens (get tokens-by-type type)]
|
||||
[:> token-group* {:key (name type)
|
||||
:tokens tokens
|
||||
:is-expanded (get open-status type false)
|
||||
:is-open (get open-status type false)
|
||||
:type type
|
||||
:selected-ids selected
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]))
|
||||
:tokens tokens}]))
|
||||
|
||||
(for [type empty-group]
|
||||
[:> token-group* {:key (name type)
|
||||
:tokens []
|
||||
:type type
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens}])]))
|
||||
:is-selected-inside-layout :is-selected-inside-layout
|
||||
:active-theme-tokens resolved-active-tokens
|
||||
:tokens []}])]))
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.files.tokens :as cfo]
|
||||
[app.common.types.shape.layout :as ctsl]
|
||||
[app.common.types.token :as ctt]
|
||||
[app.main.data.modal :as modal]
|
||||
@@ -47,9 +47,9 @@
|
||||
;; Actions ---------------------------------------------------------------------
|
||||
|
||||
(defn attribute-actions [token selected-shapes attributes]
|
||||
(let [ids-by-attributes (cft/shapes-ids-by-applied-attributes token selected-shapes attributes)
|
||||
(let [ids-by-attributes (cfo/shapes-ids-by-applied-attributes token selected-shapes attributes)
|
||||
shape-ids (into #{} (map :id selected-shapes))]
|
||||
{:all-selected? (cft/shapes-applied-all? ids-by-attributes shape-ids attributes)
|
||||
{:all-selected? (cfo/shapes-applied-all? ids-by-attributes shape-ids attributes)
|
||||
:shape-ids shape-ids
|
||||
:selected-pred #(seq (% ids-by-attributes))}))
|
||||
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.color
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.create.form-color-input-token :refer [form-color-input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value [::sm/text {:error/fn token-value-error-fn}]]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(not (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type :color}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.create-token" token-type)]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||
[:> context-notification*
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> form-color-input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,222 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.dimensions
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.create.form-input-token :refer [form-input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value [::sm/text {:error/fn token-value-error-fn}]]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(not (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type :dimensions}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.create-token" token-type)]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||
[:> context-notification*
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> form-input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,220 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.font-family
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.constants :refer [max-input-length]]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.data.workspace.tokens.propagation :as dwtp]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-combobox*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/schema:token-name assoc :error/fn #(str (:value %) (tr "workspace.tokens.token-name-validation-error")))
|
||||
[:fn {:error/fn #(tr "workspace.tokens.token-name-duplication-validation-error" (:value %))}
|
||||
#(not (ctob/token-name-path-exists? % tokens-tree))]]]
|
||||
|
||||
[:value ::sm/text]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(not (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(if token
|
||||
(update token :value cto/join-font-family)
|
||||
{:type :font-family}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(tr "workspace.tokens.create-token" token-type)]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-name"
|
||||
:name :name
|
||||
:label (tr "workspace.tokens.token-name")
|
||||
:placeholder (tr "workspace.tokens.enter-token-name" token-title)
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:auto-focus true}]
|
||||
|
||||
(when (and warning-name-change? (= action "edit"))
|
||||
[:div {:class (stl/css :warning-name-change-notification-wrapper)}
|
||||
[:> context-notification*
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> font-picker-combobox*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user