Compare commits

...

29 Commits

Author SHA1 Message Date
Eva Marco
bdcbde37c3 ♻️ Replace offsetx and offsety names 2025-12-03 10:43:00 +01:00
Eva Marco
81c6cb52ca ♻️ Rename files and components 2025-12-03 09:49:34 +01:00
Eva Marco
1dc274e6e2 ♻️ Replace shadow form 2025-12-02 14:30:53 +01:00
Luis de Dios
ef68081d1d 🎉 Add prototype tab UI tweaks (#7832)
* 🎉 Add prototype tab UI tweaks

* 📎 PR changes
2025-12-02 10:44:16 +01:00
Andrey Antukh
4ed49cdc5d Make devenv https and http2 capable (#7871)
Making it more similar on how it runs on production
environments and improves large amount of files loading
thanks to http2.
2025-12-01 20:43:23 +01:00
Andrey Antukh
50f9eedcdf Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 14:33:38 +01:00
Eva Marco
efe74e62e8 🎉 Replace font family form (#7825) 2025-12-01 11:17:25 +01:00
Eva Marco
456afe46de 🎉 Replace font family form (#7784) 2025-12-01 10:11:29 +01:00
Andrey Antukh
964ef799c2 🔥 Remove core.spec usage on common and frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
d34b6b88b6 Remove malli dev stuff from cljs build
It only used on backend.
2025-12-01 09:30:21 +01:00
Andrey Antukh
9a58f0e954 🔧 Disable code motion on shadow config 2025-12-01 09:30:21 +01:00
Andrey Antukh
adaf8be56d Use sm/coercer on app.render entry point 2025-12-01 09:30:21 +01:00
Andrey Antukh
2f1b99fa53 ♻️ Use ESM target for build frontend 2025-12-01 09:30:21 +01:00
Andrey Antukh
5080fcc594 🔥 Remove unused require of edn reader on loggin ns 2025-12-01 09:30:21 +01:00
Andrey Antukh
ea2d3758f0 Merge remote-tracking branch 'origin/staging' into develop 2025-12-01 09:28:49 +01:00
Juanfran
94c15916e2 Merge pull request #7857 from penpot/niwinz-develop-prepare-for-pnpm
 Make automatic workflows not dependent on yarn
2025-11-28 13:07:30 +01:00
Andrey Antukh
ed0f3c3595 Make automatic workflows not dependent on yarn 2025-11-28 12:26:56 +01:00
alonso.torres
c2014a37b4 🐛 Fix problem when pasting elements in reverse flex layout 2025-11-27 18:02:34 +01:00
alonso.torres
6611fbd13b 🐛 Fix problem when drag+duplicate a full grid 2025-11-27 18:02:34 +01:00
Andrey Antukh
b5a6867058 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 18:01:08 +01:00
Andrey Antukh
0f88253dd5 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 16:11:36 +01:00
Andrey Antukh
8e3996fbb0 🔧 Change concirrency rules on tests github workflow 2025-11-27 13:16:08 +01:00
Alonso Torres
67762d9450 🐛 Fix problem with worker bundling in development (#7844) 2025-11-27 13:02:47 +01:00
Andrey Antukh
7f62652870 Merge remote-tracking branch 'origin/staging' into develop 2025-11-27 09:24:40 +01:00
Andrey Antukh
78d31ab11a 🐳 Update devenv docker and compose files
Reuse the already builded imagemagick instead of building
it again on the devenv.
2025-11-26 07:44:56 +01:00
Andrey Antukh
0a80c47901 Merge remote-tracking branch 'origin/staging' into develop 2025-11-26 07:30:42 +01:00
Yamila Moreno
77f1046fc8 🔧 Add MT notification when a docker image with final tag is built (#7824) 2025-11-25 16:39:42 +01:00
Andrey Antukh
553b73a83c ♻️ Replace CircleCI with Github Actions (#7789)
* ♻️ Replace circleci with github actions

* 📎 Add integration test sharding

* 📎 Reuse single build for integration tests shards
2025-11-24 10:44:04 +01:00
Andrey Antukh
00a45cb274 📎 Bump new version on changelog 2025-11-24 09:47:00 +01:00
117 changed files with 4211 additions and 2052 deletions

View File

@@ -1,305 +0,0 @@
version: 2.1
jobs:
lint:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
steps:
- checkout
- run:
name: "fmt check"
working_directory: "."
command: |
yarn install
yarn run fmt:clj:check
- run:
name: "lint clj common"
working_directory: "."
command: |
yarn run lint:clj:common
- run:
name: "lint clj frontend"
working_directory: "."
command: |
yarn run lint:clj:frontend
- run:
name: "lint clj backend"
working_directory: "."
command: |
yarn run lint:clj:backend
- run:
name: "lint clj exporter"
working_directory: "."
command: |
yarn run lint:clj:exporter
- run:
name: "lint clj library"
working_directory: "."
command: |
yarn run lint:clj:library
test-common:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
- run:
name: "JVM tests"
working_directory: "./common"
command: |
clojure -M:dev:test
- run:
name: "NODE tests"
working_directory: "./common"
command: |
yarn install
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "common/deps.edn"}}-{{ checksum "common/yarn.lock" }}
test-frontend:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: "install dependencies"
working_directory: "./frontend"
# We install playwright here because the dependent tasks
# uses the same cache as this task so we prepopulate it
command: |
yarn install
yarn run playwright install chromium --with-deps
- run:
name: "lint scss on frontend"
working_directory: "./frontend"
command: |
yarn run lint:scss
- run:
name: "unit tests"
working_directory: "./frontend"
command: |
yarn run test
- save_cache:
paths:
- ~/.m2
- ~/.yarn
- ~/.gitlibs
- ~/.cache/ms-playwright
key: v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
test-library:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies and build
working_directory: "./library"
command: |
yarn install
- run:
name: Build and Test
working_directory: "./library"
command: |
./scripts/build
yarn run test
test-components:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx6g -Xms2g
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
# Download and cache dependencies
- restore_cache:
keys:
- v1-dependencies-{{ checksum "frontend/deps.edn"}}-{{ checksum "frontend/yarn.lock" }}
- run:
name: Install dependencies
working_directory: "./frontend"
command: |
yarn install
yarn run playwright install chromium
- run:
name: Build Storybook
working_directory: "./frontend"
command: yarn run build:storybook
- run:
name: Serve Storybook and run tests
working_directory: "./frontend"
command: |
npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
"npx http-server storybook-static --port 6006 --silent" \
"npx wait-on tcp:6006 && yarn test:storybook"
test-backend:
docker:
- image: penpotapp/devenv:latest
- image: cimg/postgres:14.5
environment:
POSTGRES_USER: penpot_test
POSTGRES_PASSWORD: penpot_test
POSTGRES_DB: penpot_test
- image: cimg/redis:7.0.5
working_directory: ~/repo
resource_class: medium+
environment:
JAVA_OPTS: -Xmx4g -Xms100m -XX:+UseSerialGC
NODE_OPTIONS: --max-old-space-size=4096
steps:
- checkout
- restore_cache:
keys:
- v1-dependencies-{{ checksum "backend/deps.edn" }}
- run:
name: "tests"
working_directory: "./backend"
command: |
clojure -M:dev:test --reporter kaocha.report/documentation
environment:
PENPOT_TEST_DATABASE_URI: "postgresql://localhost/penpot_test"
PENPOT_TEST_DATABASE_USERNAME: penpot_test
PENPOT_TEST_DATABASE_PASSWORD: penpot_test
PENPOT_TEST_REDIS_URI: "redis://localhost/1"
- save_cache:
paths:
- ~/.m2
- ~/.gitlibs
key: v1-dependencies-{{ checksum "backend/deps.edn" }}
test-render-wasm:
docker:
- image: penpotapp/devenv:latest
working_directory: ~/repo
resource_class: medium+
environment:
steps:
- checkout
- run:
name: "fmt check"
working_directory: "./render-wasm"
command: |
cargo fmt --check
- run:
name: "lint"
working_directory: "./render-wasm"
command: |
./lint
- run:
name: "cargo tests"
working_directory: "./render-wasm"
command: |
./test
workflows:
penpot:
jobs:
- test-frontend:
requires:
- lint: success
- test-library:
requires:
- lint: success
- test-components:
requires:
- lint: success
- test-backend:
requires:
- lint: success
- test-common:
requires:
- lint: success
- lint
- test-render-wasm

View File

@@ -21,6 +21,21 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs: build-docker

View File

@@ -159,17 +159,7 @@ jobs:
- name: Build Bundle
working-directory: ./frontend
run: |
corepack enable;
corepack install;
yarn install
yarn run build:app:assets
yarn run build:app
yarn run build:app:libs
- name: Build WASM
working-directory: "./render-wasm"
run: |
./build release
./scripts/build 0.0.0
- name: Store Bundle Cache
uses: actions/cache@v4
@@ -177,6 +167,7 @@ jobs:
key: "integration-bundle-${{ github.sha }}"
path: frontend/resources/public
test-integration-1:
name: "Integration Tests 1/4"
runs-on: ubuntu-24.04

View File

@@ -1,5 +1,21 @@
# CHANGELOG
## 2.13.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
### :bug: Bugs fixed
- Fix problem when drag+duplicate a full grid [Taiga #12565](https://tree.taiga.io/project/penpot/issue/12565)
- Fix problem when pasting elements in reverse flex layout [Taiga #12460](https://tree.taiga.io/project/penpot/issue/12460)
## 2.12.0 (Unreleased)
### :boom: Breaking changes & Deprecations

View File

@@ -14,8 +14,7 @@
[app.common.schema :as sm]
[clojure.core :as c]
[clojure.spec.alpha :as s]
[cuerdas.core :as str]
[expound.alpha :as expound])
[cuerdas.core :as str])
#?(:clj
(:import
clojure.lang.IPersistentMap)))
@@ -110,13 +109,6 @@
(contains? data :explain))
(explain (:explain data) opts)
(and (contains? data ::s/problems)
(contains? data ::s/value)
(contains? data ::s/spec))
(binding [s/*explain-out* expound/printer]
(with-out-str
(s/explain-out (update data ::s/problems #(take (:length opts 10) %)))))
(contains? data ::sm/explain)
(sm/humanize-explain (::sm/explain data) opts)))

View File

@@ -43,8 +43,6 @@
"
#?(:cljs (:require-macros [app.common.logging :as l]))
(:require
#?(:clj [clojure.edn :as edn]
:cljs [cljs.reader :as edn])
[app.common.data :as d]
[app.common.exceptions :as ex]
[app.common.pprint :as pp]

View File

@@ -18,7 +18,6 @@
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.types.component :as ctk]
[app.common.types.components-list :as ctkl]
[app.common.types.container :as ctn]
@@ -35,8 +34,7 @@
[app.common.types.typography :as cty]
[app.common.types.variant :as ctv]
[app.common.uuid :as uuid]
[clojure.set :as set]
[clojure.spec.alpha :as s]))
[clojure.set :as set]))
;; Change this to :info :debug or :trace to debug this module, or :warn to reset to default
(log/set-level! :warn)
@@ -473,10 +471,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync file with library"
@@ -510,10 +508,10 @@
If an asset id is given, only shapes linked to this particular asset will
be synchronized."
[changes file-id asset-type asset-id library-id libraries current-file-id]
(s/assert #{:colors :components :typographies} asset-type)
(s/assert (s/nilable ::us/uuid) asset-id)
(s/assert ::us/uuid file-id)
(s/assert ::us/uuid library-id)
(assert (contains? #{:colors :components :typographies} asset-type))
(assert (or (nil? asset-id) (uuid? asset-id)))
(assert (uuid? file-id))
(assert (uuid? library-id))
(container-log :info asset-id
:msg "Sync local components with library"
@@ -2815,13 +2813,15 @@
ids-map (into {} (map #(vector % (uuid/next))) all-ids)
;; If there is an alt-duplication of a variant, change its parent to root
;; so the copy is made as a child of root
;; If there is an alt-duplication we change to root
;; For variants so the copy is made as a child of root
;; This is because inside a variant-container can't be a copy
;; For other shape this way the layout won't be changed when duplicated
;; and if you move outside the layout will not change
shapes (map (fn [shape]
(if (and alt-duplication? (ctk/is-variant? shape))
(assoc shape :parent-id uuid/zero :frame-id nil)
shape))
(cond-> shape
alt-duplication?
(assoc :parent-id uuid/zero :frame-id uuid/zero)))
shapes)

View File

@@ -8,6 +8,8 @@
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
#?(:clj [malli.dev.pretty :as mdp])
#?(:clj [malli.dev.virhe :as v])
[app.common.data :as d]
[app.common.math :as mth]
[app.common.pprint :as pp]
@@ -19,8 +21,6 @@
[clojure.core :as c]
[cuerdas.core :as str]
[malli.core :as m]
[malli.dev.pretty :as mdp]
[malli.dev.virhe :as v]
[malli.error :as me]
[malli.generator :as mg]
[malli.registry :as mr]
@@ -245,27 +245,30 @@
:level (d/nilv level 8)
:length (d/nilv length 12)})))))
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]})
#?(:clj
(defmethod v/-format ::schemaless-explain
[_ explanation printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer)]}))
(defmethod v/-format ::explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]})
#?(:clj
(defmethod v/-format ::explain
[_ {:keys [schema] :as explanation} printer]
{:body [:group
(v/-block "Value" (v/-visit (me/error-value explanation printer) printer) printer) :break :break
(v/-block "Errors" (v/-visit (me/humanize (me/with-spell-checking explanation)) printer) printer) :break :break
(v/-block "Schema" (v/-visit schema printer) printer)]}))
(defn pretty-explain
"A helper that allows print a console-friendly output for the
explain; should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options))))
#?(:clj
(defn pretty-explain
"A helper that allows print a console-friendly output for the explain;
should not be used for other purposes"
[explain & {:keys [variant message]
:or {variant ::explain
message "Validation Error"}}]
(let [explain (fn [] (me/with-error-messages explain))]
((mdp/prettifier variant message explain default-options)))))
(defmacro ignoring
[expr]
@@ -299,6 +302,13 @@
::explain explain}))))
value))))
(defn coercer
[schema & {:as opts}]
(let [decode-fn (decoder schema json-transformer)
check-fn (check-fn schema opts)]
(fn [data]
(-> data decode-fn check-fn))))
(defn check
"A helper intended to be used on assertions for validate/check the
schema over provided data. Raises an assertion exception.
@@ -993,6 +1003,9 @@
(def valid-safe-number?
(lazy-validator ::safe-number))
(def valid-safe-int?
(lazy-validator ::safe-int))
(def valid-text?
(validator ::text))

View File

@@ -1575,10 +1575,10 @@ Will return a value that matches this schema:
(if (map? shadow)
(let [legacy-shadow-type (get "type" shadow)]
(-> shadow
(set/rename-keys {"x" :offsetX
"offsetX" :offsetX
"y" :offsetY
"offsetY" :offsetY
(set/rename-keys {"x" :offset-x
"offsetX" :offset-x
"y" :offset-y
"offsetY" :offset-y
"blur" :blur
"spread" :spread
"color" :color
@@ -1589,7 +1589,7 @@ Will return a value that matches this schema:
(= "false" %) false
(= legacy-shadow-type "innerShadow") true
:else false))
(select-keys [:offsetX :offsetY :blur :spread :color :inset])))
(select-keys [:offset-x :offset-y :blur :spread :color :inset])))
shadow))]
(cond
;; Reference value - keep as string
@@ -1860,8 +1860,8 @@ Will return a value that matches this schema:
(mapv (fn [shadow]
(if (map? shadow)
(-> shadow
(set/rename-keys {:offsetX "offsetX"
:offsetY "offsetY"
(set/rename-keys {:offset-x "offsetX"
:offset-y "offsetY"
:blur "blur"
:spread "spread"
:color "color"

View File

@@ -1897,15 +1897,15 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-single")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset false}]
(:value token)))))
(t/testing "multiple shadow token"
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-multiple")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offsetX "0", :offsetY "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(t/is (= [{:offset-x "0", :offset-y "2px", :blur "4px", :spread "0", :color "#000", :inset true}
{:offset-x "0", :offset-y "8px", :blur "16px", :spread "0", :color "#000", :inset true}]
(:value token)))))
(t/testing "shadow token with reference"
@@ -1918,7 +1918,7 @@
(let [token (ctob/get-token-by-name lib "shadow-test" "test.shadow-with-type")]
(t/is (some? token))
(t/is (= :shadow (:type token)))
(t/is (= [{:offsetX "0", :offsetY "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(t/is (= [{:offset-x "0", :offset-y "4px", :blur "8px", :spread "0", :color "rgba(0,0,0,0.2)", :inset false}]
(:value token)))))
(t/testing "shadow token with description"
@@ -1937,14 +1937,14 @@
(ctob/make-token
{:name "shadow.single"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}]
:description "A single shadow"})
"shadow.multiple"
(ctob/make-token
{:name "shadow.multiple"
:type :shadow
:value [{:offsetX "0" :offsetY "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offsetX "0" :offsetY "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
:value [{:offset-x "0" :offset-y "2px" :blur "4px" :spread "0" :color "#0000001A"}
{:offset-x "0" :offset-y "8px" :blur "16px" :spread "0" :color "#0000001A"}]})
"shadow.ref"
(ctob/make-token
{:name "shadow.ref"
@@ -1991,7 +1991,7 @@
(ctob/make-token
{:name "shadow.test"
:type :shadow
:value [{:offsetX "1" :offsetY "1" :blur "1" :spread "1" :color "red" :inset true}]
:value [{:offset-x "1" :offset-y "1" :blur "1" :spread "1" :color "red" :inset true}]
:description "Round trip test"})
"shadow.ref"
(ctob/make-token

View File

@@ -25,48 +25,6 @@ RUN set -ex; \
binutils \
build-essential autoconf libtool pkg-config
################################################################################
## IMAGE MAGICK
################################################################################
FROM base AS build-imagemagick
ENV IMAGEMAGICK_VERSION=7.1.1-47 \
DEBIAN_FRONTEND=noninteractive
RUN set -ex; \
apt-get -qq update; \
apt-get -qq upgrade; \
apt-get -qqy --no-install-recommends install \
libltdl-dev \
libpng-dev \
libjpeg-dev \
libtiff-dev \
libwebp-dev \
libopenexr-dev \
libfftw3-dev \
libzip-dev \
liblcms2-dev \
liblzma-dev \
libzstd-dev \
libheif-dev \
librsvg2-dev \
; \
rm -rf /var/lib/apt/lists/*
RUN set -eux; \
curl -LfsSo /tmp/magick.tar.gz https://github.com/ImageMagick/ImageMagick/archive/refs/tags/${IMAGEMAGICK_VERSION}.tar.gz; \
mkdir -p /tmp/magick; \
cd /tmp/magick; \
tar -xf /tmp/magick.tar.gz --strip-components=1; \
./configure --prefix=/opt/imagick; \
make -j 2; \
make install; \
rm -rf /opt/imagick/lib/libMagick++*; \
rm -rf /opt/imagick/include; \
rm -rf /opt/imagick/share;
################################################################################
## NODE SETUP
################################################################################
@@ -101,6 +59,38 @@ RUN set -eux; \
corepack enable; \
rm -rf /tmp/nodejs.tar.gz;
################################################################################
## CADDYSERVER SETUP
################################################################################
FROM base AS setup-caddy
ENV CADDY_VERSION=2.10.2
RUN set -eux; \
ARCH="$(dpkg --print-architecture)"; \
case "${ARCH}" in \
aarch64|arm64) \
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_arm64.tar.gz"; \
;; \
amd64|x86_64) \
BINARY_URL="https://github.com/caddyserver/caddy/releases/download/v${CADDY_VERSION}/caddy_${CADDY_VERSION}_linux_amd64.tar.gz"; \
;; \
*) \
echo "Unsupported arch: ${ARCH}"; \
exit 1; \
;; \
esac; \
curl -LfsSo /tmp/caddy.tar.gz ${BINARY_URL}; \
mkdir -p /tmp/caddy; \
cd /tmp/caddy; \
tar -xf /tmp/caddy.tar.gz; \
chown -R root /tmp/caddy; \
mv /tmp/caddy/caddy /usr/bin/; \
rm -rf /tmp/caddy.tar.gz; \
rm -rf /tmp/caddy;
################################################################################
## JVM SETUP
################################################################################
@@ -385,7 +375,7 @@ ENV LANG='C.UTF-8' \
RUSTUP_HOME="/opt/rustup" \
PATH="/opt/jdk/bin:/opt/utils/bin:/opt/clojure/bin:/opt/node/bin:/opt/imagick/bin:/opt/cargo/bin:$PATH"
COPY --from=build-imagemagick /opt/imagick /opt/imagick
COPY --from=penpotapp/imagemagick:7.1.2-0 /opt/imagick /opt/imagick
COPY --from=setup-jvm /opt/jdk /opt/jdk
COPY --from=setup-jvm /opt/clojure /opt/clojure
COPY --from=setup-node /opt/node /opt/node
@@ -393,6 +383,7 @@ COPY --from=setup-utils /opt/utils /opt/utils
COPY --from=setup-rust /opt/cargo /opt/cargo
COPY --from=setup-rust /opt/rustup /opt/rustup
COPY --from=setup-rust /opt/emsdk /opt/emsdk
COPY --from=setup-caddy /usr/bin/caddy /usr/bin/caddy
COPY files/nginx.conf /etc/nginx/nginx.conf
COPY files/nginx-mime.types /etc/nginx/mime.types
@@ -403,6 +394,7 @@ COPY files/vimrc /root/.vimrc
COPY files/tmux.conf /root/.tmux.conf
COPY files/sudoers /etc/sudoers
COPY files/Caddyfile /home/
COPY files/start-tmux.sh /home/start-tmux.sh
COPY files/start-tmux-back.sh /home/start-tmux-back.sh
COPY files/entrypoint.sh /home/entrypoint.sh

View File

@@ -67,6 +67,11 @@ services:
- PENPOT_LDAP_ATTRS_FULLNAME=cn
- PENPOT_LDAP_ATTRS_PHOTO=jpegPhoto
networks:
default:
aliases:
- main
minio:
image: "minio/minio:RELEASE.2025-04-03T14-56-28Z"
command: minio server /mnt/data --console-address ":9001"
@@ -78,10 +83,6 @@ services:
- MINIO_ROOT_USER=minioadmin
- MINIO_ROOT_PASSWORD=minioadmin
ports:
- 9000:9000
- 9001:9001
networks:
default:
aliases:

View File

@@ -0,0 +1,4 @@
localhost:3449 {
tls internal
reverse_proxy localhost:4449
}

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
set -e
nginx
tail -f /dev/null
nginx;
caddy run -c /home/Caddyfile;

View File

@@ -12,7 +12,7 @@ http {
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 0;
keepalive_timeout 100;
types_hash_max_size 2048;
server_tokens off;
@@ -55,7 +55,7 @@ http {
proxy_cache_key "$host$request_uri";
server {
listen 3449 default_server;
listen 4449 default_server;
server_name _;
client_max_body_size 300M;
@@ -231,7 +231,6 @@ http {
}
add_header Cache-Control "no-store";
add_header Connection close always;
# This header is what we need to use on prod
# add_header Cache-Control "public, must-revalidate, max-age=0";
try_files $uri /index.html$is_args$args /index.html =404;

View File

@@ -20,8 +20,8 @@
:git/url "https://github.com/funcool/beicon.git"}
funcool/rumext
{:git/tag "v2.24"
:git/sha "17a0c94"
{:git/tag "v2.25"
:git/sha "27e5a1a"
:git/url "https://github.com/funcool/rumext.git"}
instaparse/instaparse {:mvn/version "1.5.0"}
@@ -42,7 +42,7 @@
:dev
{:extra-paths ["dev"]
:extra-deps
{thheller/shadow-cljs {:mvn/version "3.2.0"}
{thheller/shadow-cljs {:mvn/version "3.2.2"}
com.bhauman/rebel-readline {:mvn/version "RELEASE"}
org.clojure/tools.namespace {:mvn/version "RELEASE"}
criterium/criterium {:mvn/version "RELEASE"}

View File

@@ -45,9 +45,9 @@
"translations": "node ./scripts/translations.js",
"watch:app:assets": "node ./scripts/watch.js",
"watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main storybook",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs",
"watch:app": "yarn run clear:shadow-cache && yarn run build:app:worker && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch": "yarn run watch:app:assets",
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"

View File

@@ -22,9 +22,9 @@ export default defineConfig({
workers: 1,
/* Timeout for expects (longer in CI) */
timeout: 60000,
timeout: 80000,
expect: {
timeout: process.env.CI ? 30000 : 5000,
timeout: process.env.CI ? 40000 : 5000,
},
/* Reporter to use. See https://playwright.dev/docs/test-reporters */

View File

@@ -5947,8 +5947,8 @@
"~:spread": "10",
"~:color": "rgb(160, 73, 73)",
"~:inset": true,
"~:offsetX": "10",
"~:offsetY": "10"
"~:offset-x": "10",
"~:offset-y": "10"
}
],
"~:description": "",

View File

@@ -73,7 +73,7 @@ export class BasePage {
}
static async mockConfigFlags(page, flags) {
const url = "**/js/config.js?ts=*";
const url = "**/js/config.js";
return await page.route(url, (route) =>
route.fulfill({
status: 200,

View File

@@ -303,7 +303,7 @@ test.describe("Tokens: Tokens Tab", () => {
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.pressSequentially(".changed");
await tokensUpdateCreateModal.getByRole("button", {name: "Save"}).click();
await tokensUpdateCreateModal.getByRole("button", { name: "Save" }).click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
@@ -1070,6 +1070,7 @@ test.describe("Tokens: Apply token", () => {
// Fill in values for all fields and verify they persist when switching tabs
await fontSizeField.fill("16");
await expect(saveButton).toBeEnabled();
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
@@ -1238,8 +1239,12 @@ test.describe("Tokens: Apply token", () => {
// Fill in the shadow values
const offsetXInput = firstShadowFields.getByLabel("X");
const offsetYInput = firstShadowFields.getByLabel("Y");
const blurInput = firstShadowFields.getByLabel("Blur");
const spreadInput = firstShadowFields.getByLabel("Spread");
const blurInput = firstShadowFields.getByRole("textbox", {
name: "Blur",
});
const spreadInput = firstShadowFields.getByRole("textbox", {
name: "Spread",
});
await offsetXInput.fill("2");
await offsetYInput.fill("2");
@@ -1260,9 +1265,10 @@ test.describe("Tokens: Apply token", () => {
await valueSaturationSelector.click({ position: { x: 50, y: 50 } });
// Verify that a color value was set
const colorInput = firstShadowFields.getByLabel("Color");
const firstColorValue = await colorInput.inputValue();
await expect(firstColorValue).toMatch(/^rgb(.*)$/);
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
await expect(colorInput).toHaveValue(/^rgb(.*)$/);
// Wait for validation to complete
await expect(
@@ -1280,11 +1286,15 @@ test.describe("Tokens: Apply token", () => {
const firstShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-0",
);
const colorInput = firstShadowFields.getByLabel("Color");
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
const firstColorValue = await colorInput.inputValue();
// User adds a second shadow
const addButton = firstShadowFields.getByTestId("shadow-add-button-0");
const addButton = tokensUpdateCreateModal.getByRole("button", {
name: "Add Shadow",
});
await addButton.click();
const secondShadowFields = tokensUpdateCreateModal.getByTestId(
@@ -1293,8 +1303,7 @@ test.describe("Tokens: Apply token", () => {
await expect(secondShadowFields).toBeVisible();
// User adds a third shadow
const addButton2 = secondShadowFields.getByTestId("shadow-add-button-1");
await addButton2.click();
await addButton.click();
const thirdShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-2",
@@ -1304,9 +1313,15 @@ test.describe("Tokens: Apply token", () => {
// User adds values for the third shadow
const thirdOffsetXInput = thirdShadowFields.getByLabel("X");
const thirdOffsetYInput = thirdShadowFields.getByLabel("Y");
const thirdBlurInput = thirdShadowFields.getByLabel("Blur");
const thirdSpreadInput = thirdShadowFields.getByLabel("Spread");
const thirdColorInput = thirdShadowFields.getByLabel("Color");
const thirdBlurInput = thirdShadowFields.getByRole("textbox", {
name: "Blur",
});
const thirdSpreadInput = thirdShadowFields.getByRole("textbox", {
name: "Spread",
});
const thirdColorInput = thirdShadowFields.getByRole("textbox", {
name: "Color",
});
await thirdOffsetXInput.fill("10");
await thirdOffsetYInput.fill("10");
@@ -1315,15 +1330,13 @@ test.describe("Tokens: Apply token", () => {
await thirdColorInput.fill("#FF0000");
// User removes the 2nd shadow
const removeButton2 = secondShadowFields.getByTestId(
"shadow-remove-button-1",
);
const removeButton2 = secondShadowFields.getByRole("button", {
name: "Remove Shadow",
});
await removeButton2.click();
// Verify second shadow is removed
await expect(
secondShadowFields.getByTestId("shadow-add-button-3"),
).not.toBeVisible();
// Verify that we have only two shadow fields
await expect(thirdShadowFields).not.toBeVisible();
// Verify that the first shadow kept its values
const firstOffsetXValue = await firstShadowFields
@@ -1333,13 +1346,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const firstBlurValue = await firstShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const firstSpreadValue = await firstShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const firstColorValueAfter = await firstShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(firstOffsetXValue).toBe("2");
@@ -1348,7 +1361,7 @@ test.describe("Tokens: Apply token", () => {
await expect(firstSpreadValue).toBe("0");
await expect(firstColorValueAfter).toBe(firstColorValue);
// Verify that the third shadow (now second) kept its values
// Verify that the second kept its values (after shadow 3)
// After removing index 1, the third shadow becomes the second shadow at index 1
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-1",
@@ -1362,13 +1375,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const secondBlurValue = await newSecondShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const secondSpreadValue = await newSecondShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const secondColorValue = await newSecondShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(secondOffsetXValue).toBe("10");
@@ -1385,7 +1398,9 @@ test.describe("Tokens: Apply token", () => {
const newSecondShadowFields = tokensUpdateCreateModal.getByTestId(
"shadow-input-fields-1",
);
const colorInput = firstShadowFields.getByLabel("Color");
const colorInput = firstShadowFields.getByRole("textbox", {
name: "Color",
});
const firstColorValue = await colorInput.inputValue();
// Switch to reference tab
@@ -1413,13 +1428,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const restoredFirstBlur = await firstShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const restoredFirstSpread = await firstShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const restoredFirstColor = await firstShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(restoredFirstOffsetX).toBe("2");
@@ -1436,13 +1451,13 @@ test.describe("Tokens: Apply token", () => {
.getByLabel("Y")
.inputValue();
const restoredSecondBlur = await newSecondShadowFields
.getByLabel("Blur")
.getByRole("textbox", { name: "Blur" })
.inputValue();
const restoredSecondSpread = await newSecondShadowFields
.getByLabel("Spread")
.getByRole("textbox", { name: "Spread" })
.inputValue();
const restoredSecondColor = await newSecondShadowFields
.getByLabel("Color")
.getByRole("textbox", { name: "Color" })
.inputValue();
await expect(restoredSecondOffsetX).toBe("10");

View File

@@ -25,14 +25,14 @@
<link rel="icon" href="images/favicon.png" />
{{# manifest}}
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
<script defer src="{{& config}}"></script>
<script defer src="{{& polyfills}}"></script>
<script src="{{& config}}"></script>
<script src="{{& polyfills}}"></script>
{{/manifest}}
<script>
window.penpotTranslations = JSON.parse({{& translations}});
window.penpotVersion = "%version%";
window.penpotBuildDate = "%buildDate%";
<script type="module">
globalThis.penpotTranslations = JSON.parse({{& translations}});
globalThis.penpotVersion = "%version%";
globalThis.penpotBuildDate = "%buildDate%";
</script>
<!--cookie-consent-->
</head>
@@ -44,9 +44,11 @@
<section id="modal"></section>
{{# manifest}}
<script defer src="js/libs.js?ts={{& ts}}"></script>
<script defer src="{{& shared}}"></script>
<script defer src="{{& main}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& main}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -19,9 +19,11 @@
</head>
<body>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& rasterizer}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& rasterizer}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -18,9 +18,11 @@
<body>
<div id="app"></div>
{{# manifest}}
<script src="js/libs.js?ts={{& ts}}"></script>
<script src="{{& shared}}"></script>
<script src="{{& render}}"></script>
<script type="module" src="{{& libs}}"></script>
<script type="module">
import { init } from "{{& render}}";
init();
</script>
{{/manifest}}
</body>
</html>

View File

@@ -193,26 +193,30 @@ async function readShadowManifest() {
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
config: "./js/config.js",
polyfills: "./js/polyfills.js",
worker_main: "./js/worker/main.js",
libs: "./js/libs.js",
};
for (let item of content) {
index[item.name] = "js/" + item["output-name"];
index[item.name] = "./js/" + item["output-name"] + "";
}
return index;
} catch (cause) {
return {
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
main: "js/main.js?ts=" + ts,
shared: "js/shared.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
rasterizer: "js/rasterizer.js?ts=" + ts,
config: "./js/config.js",
polyfills: "./js/polyfills.js",
main: "./js/main.js",
shared: "./js/shared.js",
worker_main: "./js/worker/main.js",
rasterizer: "./js/rasterizer.js",
libs: "./js/libs.js",
};
return index;
}
}

View File

@@ -26,7 +26,10 @@ yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
mkdir -p resources/public;
mkdir -p target/dist;
yarn run build:app:main $EXTRA_PARAMS || exit 1
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
@@ -35,19 +38,18 @@ fi
yarn run build:app:libs || exit 1;
yarn run build:app:assets || exit 1;
mkdir -p target/dist;
rsync -avr resources/public/ target/dist/
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/index.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/render.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/rasterizer.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/index.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/render.html;
sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./resources/public/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./resources/public/rasterizer.html;
if [ "$INCLUDE_WASM" = "yes" ]; then
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./resources/public/js/render_wasm.js;
fi
rsync -avr resources/public/ target/dist/;
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook
yarn run build:storybook || exit 1;

View File

@@ -31,9 +31,9 @@ const rebuildNotify = {
const config = {
entryPoints: ["target/index.js"],
bundle: true,
format: "iife",
format: "esm",
banner: {
js: '"use strict"; var global = globalThis;',
js: '"use strict";\nvar global = globalThis;',
},
outfile: "resources/public/js/libs.js",
plugins: [fixReactVirtualized, rebuildNotify],

View File

@@ -6,13 +6,12 @@
:builds
{:main
{:target :browser
{:target :esm
:output-dir "resources/public/js/"
:asset-path "/js"
:devtools {:watch-dir "resources/public"
:reload-strategy :full}
:build-options {:manifest-name "manifest.json"}
:module-loader true
:modules
{:shared
{:entries []}
@@ -20,7 +19,7 @@
:main
{:entries [app.main app.plugins.api]
:depends-on #{:shared}
:init-fn app.main/init}
:exports {init app.main/init}}
:util-highlight
{:entries [app.util.code-highlight]
@@ -50,12 +49,12 @@
:render
{:entries [app.render]
:depends-on #{:shared}
:init-fn app.render/init}
:exports {init app.render/init}}
:rasterizer
{:entries [app.rasterizer]
:depends-on #{:shared}
:init-fn app.rasterizer/init}}
:exports {init app.rasterizer/init}}}
:js-options
{:entry-keys ["module" "browser" "main"]
@@ -75,11 +74,10 @@
:compiler-options
{:fn-invoke-direct true
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
:output-wrapper true
:rename-prefix-namespace "PENPOT"
:source-map true
:elide-asserts true
:anon-fn-naming-policy :off
:cross-chunk-method-motion false
:source-map-detail-level :all}}}
:worker

View File

@@ -8,7 +8,6 @@
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.spec :as us]
[app.common.types.profile :refer [schema:profile]]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -484,7 +483,7 @@
(defn delete-access-token
[{:keys [id] :as params}]
(us/assert! ::us/uuid id)
(assert (uuid? id))
(ptk/reify ::delete-access-token
ptk/WatchEvent
(watch [_ _ _]

View File

@@ -368,8 +368,8 @@
(let [add-keyed-errors (fn [shadow-result k errors]
(update shadow-result :errors concat
(map #(assoc % :shadow-key k :shadow-index shadow-index) errors)))
parsers {:offsetX parse-sd-token-general-value
:offsetY parse-sd-token-general-value
parsers {:offset-x parse-sd-token-general-value
:offset-y parse-sd-token-general-value
:blur parse-sd-token-shadow-blur
:spread parse-sd-token-shadow-spread
:color parse-sd-token-color-value
@@ -389,35 +389,42 @@
(defn- parse-sd-token-shadow-value
"Parses shadow value and validates it."
[value]
(cond
;; Reference value (string)
(string? value) {:value value}
(let [missing-references
(when (string? value)
(seq (cto/find-token-value-references value)))]
(cond
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
(string? value)
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-shadow value)]}
;; Empty value
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
(nil? value) {:errors [(wte/get-error-code :error.token/empty-input)]}
;; Invalid value
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
(not (js/Array.isArray value)) {:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value value)]}
;; Array of shadows
:else
(let [converted (js->clj value :keywordize-keys true)
:else
(let [converted (js->clj value :keywordize-keys true)
;; Parse each shadow with its index
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
parsed-shadows (map-indexed
(fn [idx shadow-map]
(parse-single-shadow shadow-map idx))
converted)
;; Collect all errors from all shadows
all-errors (mapcat :errors parsed-shadows)
all-errors (mapcat :errors parsed-shadows)
;; Collect all values from shadows that have values
all-values (into [] (keep :value parsed-shadows))]
all-values (into [] (keep :value parsed-shadows))]
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values}))))
(if (seq all-errors)
{:errors all-errors
:value all-values}
{:value all-values})))))
(defn collect-shadow-errors [token shadow-index]
(group-by :shadow-key

View File

@@ -706,53 +706,58 @@
(= 1 (count tree-root)))]
(cond
;; Paste next to selected frame, if selected is itself or of the same size as the copied
(and (selected-frame? state)
(or (any-same-frame-from-selected? state (keys pobjects))
(and only-one-root-shape?
(frame-same-size? pobjects (first tree-root)))))
(let [selected-frame-obj (get page-objects (first page-selected))
parent-id (:parent-id base)
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
paste-y (:y selected-frame-obj)
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
[parent-id delta index])
;; Paste inside selected frame otherwise
(selected-frame? state)
(let [selected-frame-obj (get page-objects (first page-selected))
origin-frame-id (:frame-id first-selected-obj)
origin-frame-object (get page-objects origin-frame-id)
(if (or (any-same-frame-from-selected? state (keys pobjects))
(and only-one-root-shape?
(frame-same-size? pobjects (first tree-root))))
;; Paste next to selected frame, if selected is itself or of the same size as the copied
(let [selected-frame-obj (get page-objects (first page-selected))
parent-id (:parent-id base)
paste-x (+ (:width selected-frame-obj) (:x selected-frame-obj) 50)
paste-y (:y selected-frame-obj)
delta (gpt/subtract (gpt/point paste-x paste-y) orig-pos)]
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width wrapper))))
[parent-id delta index])
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
;; Paste inside selected frame otherwise
(let [selected-frame-obj (get page-objects (first page-selected))
origin-frame-id (:frame-id first-selected-obj)
origin-frame-object (get page-objects origin-frame-id)
;; Pasted objects mustn't exceed the selected frame x limit
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
(:x frame-object))
margin-x (-> (- (:width origin-frame-object) (+ (:x wrapper) (:width wrapper)))
(min (- (:width frame-object) (:width wrapper))))
;; Pasted objects mustn't exceed the selected frame y limit
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
(:y frame-object))
margin-y (-> (- (:height origin-frame-object) (+ (:y wrapper) (:height wrapper)))
(min (- (:height frame-object) (:height wrapper))))
delta (if (= origin-frame-id uuid/zero)
;; When the origin isn't in a frame the result is pasted in the center.
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
;; When pasting from one frame to another frame the object
;; position must be limited to container boundaries. If
;; the pasted object doesn't fit we try to:
;;
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right
;; and bottom in the original frame
(gpt/point paste-x paste-y))
;; Pasted objects mustn't exceed the selected frame x limit
paste-x (if (> (+ (:width wrapper) (:x1 wrapper)) (:width frame-object))
(+ (- (:x frame-object) (:x orig-pos)) (- (:width frame-object) (:width wrapper) margin-x))
(:x frame-object))
;; Pasted objects mustn't exceed the selected frame y limit
paste-y (if (> (+ (:height wrapper) (:y1 wrapper)) (:height frame-object))
(+ (- (:y frame-object) (:y orig-pos)) (- (:height frame-object) (:height wrapper) margin-y))
(:y frame-object))
delta (if (= origin-frame-id uuid/zero)
;; When the origin isn't in a frame the result is pasted in the center.
(gpt/subtract (gsh/shape->center frame-object) (grc/rect->center wrapper))
;; When pasting from one frame to another frame the object
;; position must be limited to container boundaries. If
;; the pasted object doesn't fit we try to:
;;
;; - Align it to the limits on the x and y axis
;; - Respect the distance of the object to the right
;; and bottom in the original frame
(gpt/point paste-x paste-y))]
[frame-id delta (dec (count (:shapes selected-frame-obj)))]))
target-index
(if (and (ctl/flex-layout? selected-frame-obj) (ctl/reverse? selected-frame-obj))
(dec 0) ;; Before the first index 0
(count (:shapes selected-frame-obj)))]
[frame-id delta target-index])
(empty? page-selected)
(let [frame-id (ctst/top-nested-frame page-objects position)

View File

@@ -119,21 +119,6 @@
(let [page (dsh/lookup-page state)]
(rx/of (update-flow (:id page) flow-id #(assoc % :name name)))))))
(defn start-rename-flow
[id]
(dm/assert! (uuid? id))
(ptk/reify ::start-rename-flow
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-local :flow-for-rename] id))))
(defn end-rename-flow
[]
(ptk/reify ::end-rename-flow
ptk/UpdateEvent
(update [_ state]
(update state :workspace-local dissoc :flow-for-rename))))
;; --- Interactions
(defn- connected-frame?

View File

@@ -153,11 +153,11 @@
(defn value->shadow
"Transform a token shadow value into penpot shadow data structure"
[value]
(mapv (fn [{:keys [offsetX offsetY blur spread color inset]}]
(mapv (fn [{:keys [offset-x offset-y blur spread color inset]}]
{:id (random-uuid)
:hidden false
:offset-x offsetX
:offset-y offsetY
:offset-x offset-x
:offset-y offset-y
:blur blur
:color (value->color color)
:spread spread

View File

@@ -108,6 +108,10 @@
{:error/code :error.style-dictionary/invalid-token-value-shadow-spread
:error/fn #(tr "workspace.tokens.shadow-spread-range")}
:error.style-dictionary/invalid-token-value-shadow
{:error/code :error.style-dictionary/invalid-token-value-shadow
:error/fn #(tr "workspace.tokens.invalid-token-value-shadow" %)}
:error/unknown
{:error/code :error/unknown
:error/fn #(tr "labels.unknown-error")}})

View File

@@ -16,8 +16,8 @@
:letter-spacing "Letter Spacing"
:text-case "Text Case"
:text-decoration "Text Decoration"
:offsetX "X"
:offsetY "Y"
:offset-x "X"
:offset-y "Y"
:blur "Blur"
:spread "Spread"
:color "Color"

View File

@@ -31,27 +31,28 @@
[app.main.ui.static :as static]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.modules :as mod]
[app.util.theme :as theme]
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
(def auth-page
(mf/lazy-component app.main.ui.auth/auth))
(mf/lazy #(mod/load 'app.main.ui.auth/auth-page*)))
(def verify-token-page
(mf/lazy-component app.main.ui.auth.verify-token/verify-token))
(def verify-token-page*
(mf/lazy #(mod/load 'app.main.ui.auth.verify-token/verify-token-page*)))
(def viewer-page*
(mf/lazy-component app.main.ui.viewer/viewer*))
(mf/lazy #(mod/load 'app.main.ui.viewer/viewer-page*)))
(def dashboard-page*
(mf/lazy-component app.main.ui.dashboard/dashboard*))
(mf/lazy #(mod/load 'app.main.ui.dashboard/dashboard-page*)))
(def settings-page*
(mf/lazy-component app.main.ui.settings/settings*))
(mf/lazy #(mod/load 'app.main.ui.settings/settings-page*)))
(def workspace-page*
(mf/lazy-component app.main.ui.workspace/workspace*))
(mf/lazy #(mod/load 'app.main.ui.workspace/workspace-page*)))
(mf/defc workspace-legacy-redirect*
{::mf/props :obj
@@ -189,7 +190,7 @@
[:? [:& auth-page {:route route}]]
:auth-verify-token
[:? [:& verify-token-page {:route route}]]
[:? [:& verify-token-page* {:route route}]]
(:settings-profile
:settings-password

View File

@@ -19,8 +19,7 @@
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc auth
{::mf/props :obj}
(mf/defc auth*
[{:keys [route]}]
(let [section (dm/get-in route [:data :name])
is-register (or
@@ -69,3 +68,9 @@
(when (= section :auth-register)
[:& terms-register])]]))
(mf/defc auth-page*
{::mf/lazy-load true}
[props]
[:> auth* props])

View File

@@ -61,9 +61,9 @@
(rt/nav :auth-login)
(ntf/warn (tr "errors.unexpected-token"))))
(mf/defc verify-token
[{:keys [route] :as props}]
(let [token (get-in route [:query-params :token])
(mf/defc verify-token*
[{:keys [route]}]
(let [token (get-in route [:query-params :token])
bad-token (mf/use-state false)]
(mf/with-effect []
@@ -99,3 +99,8 @@
[:> static/invalid-token {}]
[:> loader* {:title (tr "labels.loading")
:overlay true}])))
(mf/defc verify-token-page*
{::mf/lazy-load true}
[props]
[:> verify-token* props])

View File

@@ -8,24 +8,28 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.util.modules :as modules]
[cuerdas.core :as str]
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.lazy :as lazy]))
[rumext.v2 :as mf]))
(def highlight-fn
(lazy/loadable app.util.code-highlight/highlight!))
(delay (modules/load-fn 'app.util.code-highlight/highlight!)))
(mf/defc code-block
{::mf/wrap-props false}
[{:keys [code type]}]
(let [block-ref (mf/use-ref)
code (str/trim code)]
code (str/trim code)]
(mf/with-effect [code type]
(when-let [node (mf/ref-val block-ref)]
(p/let [highlight-fn (lazy/load highlight-fn)]
(highlight-fn node))))
(->> @highlight-fn
(p/fmap (fn [f] (f)))
(p/fnly (fn [f cause]
(if cause
(js/console.error cause)
(f node)))))))
[:pre {:class (dm/str type " " (stl/css :code-display)) :ref block-ref} code]))

View File

@@ -9,6 +9,7 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.ui.ds.foundations.assets.icon :refer [icon*]]
[app.main.ui.formats :as fmt]
[app.util.dom :as dom]
[rumext.v2 :as mf]))
@@ -46,7 +47,7 @@
:disabled disabled)}
(if (some? icon)
[:span {:class icon-class} icon]
[:> icon* {:icon-id icon :class icon-class :aria-hidden true}]
[:span {:class (stl/css :title-name)} value])
[:input {:id id

View File

@@ -247,7 +247,6 @@
(swap! storage/session dissoc :template))))))
(mf/defc dashboard*
{::mf/props :obj}
[{:keys [profile project-id team-id search-term plugin-url template section]}]
(let [team (mf/deref refs/team)
projects (mf/deref refs/projects)
@@ -313,3 +312,8 @@
:section section
:search-term search-term
:team team}]]]))
(mf/defc dashboard-page*
{::mf/lazy-load true}
[props]
[:> dashboard* props])

View File

@@ -9,6 +9,7 @@
[app.config :as cf]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.checkbox :refer [checkbox*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
@@ -62,6 +63,7 @@
:RawSvg raw-svg*
:Select select*
:Switch switch*
:Checkbox checkbox*
:Combobox combobox*
:Text text*
:TabSwitcher tab-switcher*

View File

@@ -11,12 +11,18 @@
%base-button {
--button-bg-color: initial;
--button-fg-color: initial;
--button-hover-bg-color: initial;
--button-hover-fg-color: initial;
--button-active-bg-color: initial;
--button-active-fg-color: initial;
--button-disabled-bg-color: initial;
--button-disabled-fg-color: initial;
--button-border-color: var(--button-bg-color);
--button-focus-inner-ring-color: initial;
--button-focus-outer-ring-color: initial;
@@ -38,8 +44,10 @@
--button-fg-color: var(--button-hover-fg-color);
}
&:active {
&:active,
&[aria-pressed="true"] {
--button-bg-color: var(--button-active-bg-color);
--button-fg-color: var(--button-active-fg-color);
}
&:focus-visible {
@@ -63,6 +71,7 @@
--button-hover-fg-color: var(--color-background-secondary);
--button-active-bg-color: var(--color-accent-tertiary);
--button-active-fg-color: var(--color-background-secondary);
--button-disabled-bg-color: var(--color-accent-primary-muted);
--button-disabled-fg-color: var(--color-background-secondary);
@@ -72,7 +81,8 @@
--button-focus-inner-ring-color: var(--color-background-secondary);
--button-focus-outer-ring-color: var(--color-accent-primary);
&:active {
&:active,
&[aria-pressed="true"] {
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
}
}
@@ -85,6 +95,7 @@
--button-hover-fg-color: var(--color-accent-primary);
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-foreground-secondary);
@@ -103,6 +114,7 @@
--button-hover-fg-color: var(--color-accent-primary);
--button-active-bg-color: var(--color-background-quaternary);
--button-active-fg-color: var(--color-accent-primary);
--button-disabled-bg-color: transparent;
--button-disabled-fg-color: var(--color-accent-primary-muted);
@@ -121,6 +133,7 @@
--button-hover-fg-color: var(--color-foreground-primary);
--button-active-bg-color: var(--color-accent-error);
--button-active-fg-color: var(--color-foreground-primary);
--button-disabled-bg-color: var(--color-background-error);
--button-disabled-fg-color: var(--color-accent-error);
@@ -130,7 +143,8 @@
--button-focus-inner-ring-color: var(--color-background-primary);
--button-focus-outer-ring-color: var(--color-accent-primary);
&:active {
&:active,
&[aria-pressed="true"] {
box-shadow: inset 0 0 #{px2rem(10)} #{px2rem(2)} rgba(0, 0, 0, 0.2);
}
}

View File

@@ -0,0 +1,45 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.controls.checkbox
(: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:checkbox
[:map
[:id {:optional true} :string]
[:label {:optional true} :string]
[:checked {:optional true} :boolean]
[:on-change {:optional true} fn?]
[:disabled {:optional true} :boolean]])
(mf/defc checkbox*
{::mf/schema schema:checkbox}
[{:keys [id class label checked on-change disabled] :rest props}]
(let [props
(mf/spread-props props {:type "checkbox"
:class (stl/css :checkbox-input)
:id id
:checked checked
:on-change on-change
:disabled disabled})]
[:div {:class [class (stl/css :checkbox)]}
[:label {:for id
:class (stl/css :checkbox-label)}
[:div {:class (stl/css-case :checkbox-box true
:checked checked
:disabled disabled)}
(when checked
[:> icon* {:icon-id i/tick
:size "s"}])]
[:div {:class (stl/css :checkbox-text)} label]
[:> :input props]]]))

View File

@@ -0,0 +1,40 @@
{ /* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as Checkbox from "./checkbox.stories";
<Meta title="Controls/Checkbox" />
# Checkbox
The `checkbox*` component is a toggle control. It allows users to switch between boolean states (`false` or `true`).
<Canvas of={Checkbox.Default} />
<Canvas of={Checkbox.Checked} />
## Anatomy
The checkbox component consists of three main parts:
- **Label** (optional): the text that describes what the checkbox controls. Clicking on this text also works for toggling.
- **Box**: the box which shows the current state. Contains a check if the state is `true`.
- **Native element**: the native HTML element which holds the state. It remains hidden to the user.
## Usage Guidelines
### When to Use
- For boolean settings that take effect immediately.
- In preference panels and configuration screens.
### When Not to Use
- For actions that require confirmation (use buttons instead).
- For multiple choice selections (use radio buttons or select).
- For temporary states that need explicit "Apply" action.
- For ternary states.

View File

@@ -0,0 +1,86 @@
// 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 *;
.checkbox {
--input-checkbox-border-color: var(--color-foreground-secondary);
--input-checkbox-border-color-focus: var(--color-accent-primary);
--input-checkbox-border-color-hover: var(--color-accent-primary-muted);
--input-checkbox-foreground-color: var(--color-foreground-primary);
--input-checkbox-background-color: var(--color-background-quaternary);
--input-checkbox-border-color-checked: var(--color-background-quaternary);
--input-checkbox-foreground-color-checked: var(--color-background-primary);
--input-checkbox-background-color-checked: var(--color-accent-primary);
--input-checkbox-foreground-color-disabled: var(--color-background-primary);
--input-checkbox-background-color-disabled: var(--color-foreground-secondary);
--input-checkbox-text-color: var(--color-foreground-secondary);
}
.checkbox-label {
display: grid;
grid-template-columns: var(--sp-l) 1fr 0;
align-items: center;
&:hover {
.checkbox-box {
border-color: var(--input-checkbox-border-color-hover);
}
}
&:focus,
&:focus-within {
.checkbox-box {
border-color: var(--input-checkbox-border-color-focus);
}
}
}
.checkbox-box {
display: flex;
align-items: center;
justify-content: center;
inline-size: $sz-16;
block-size: $sz-16;
border-radius: $br-4;
border: $b-1 solid var(--input-checkbox-border-color);
color: var(--input-checkbox-foreground-color);
background-color: var(--input-checkbox-background-color);
&.disabled {
border: 0;
}
&.checked {
--input-checkbox-border-color: var(--input-checkbox-border-color-checked);
--input-checkbox-foreground-color: var(--input-checkbox-foreground-color-checked);
--input-checkbox-background-color: var(--input-checkbox-background-color-checked);
&.disabled {
--input-checkbox-foreground-color: var(--input-checkbox-foreground-color-disabled);
--input-checkbox-background-color: var(--input-checkbox-background-color-disabled);
}
}
}
.checkbox-text {
@include use-typography("body-small");
padding-inline-start: var(--sp-s);
color: var(--input-checkbox-text-color);
}
.checkbox-input {
&:focus {
outline: 0;
inline-size: 0;
block-size: 0;
}
}

View File

@@ -0,0 +1,76 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
import * as React from "react";
import Components from "@target/components";
const { Checkbox } = Components;
export default {
title: "Controls/Checkbox",
component: Checkbox,
argTypes: {
label: {
control: { type: "text" },
description: "Label text displayed next to the checkbox",
},
checked: {
control: { type: "boolean" },
description: "Whether the checkbox is checked",
},
disabled: {
control: { type: "boolean" },
description: "Whether the checkbox is disabled",
},
},
args: {
checked: false,
disabled: false,
},
parameters: {
controls: {
exclude: ["id", "on-change"],
},
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const Default = {
args: {
label: "Toggle something",
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const Checked = {
args: {
label: "Toggle something",
checked: true,
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const WithoutLabel = {
args: {
disabled: false,
},
render: ({ ...args }) => <Checkbox {...args} />,
};
export const WithLongLabel = {
args: {
label:
"This is a very long label that demonstrates how the checkbox component handles text wrapping and layout when the label content is extensive",
disabled: false,
},
render: ({ ...args }) => (
<div style={{ maxWidth: "300px" }}>
<Checkbox {...args} />
</div>
),
};

View File

@@ -26,14 +26,19 @@
[:max-length {:optional true} :int]
[:variant {:optional true} [:maybe [:enum "seamless" "dense" "comfortable"]]]
[:hint-message {:optional true} [:maybe :string]]
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]])
[:hint-type {:optional true} [:maybe [:enum "hint" "error" "warning"]]]
[:hint-formated {:optional true} :boolean]])
(mf/defc input*
{::mf/forward-ref true
::mf/schema schema:input}
[{:keys [id class label is-optional type max-length variant hint-message hint-type] :rest props} ref]
[{:keys [id class label is-optional type max-length variant hint-message hint-type hint-formated] :rest props} ref]
(let [id (or id (mf/use-id))
variant (d/nilv variant "dense")
hint-class (if (and (not= "error" hint-type)
hint-formated)
(stl/css :hint-formated)
"")
is-optional (d/nilv is-optional false)
type (d/nilv type "text")
max-length (d/nilv max-length max-input-length)
@@ -56,6 +61,7 @@
[:> input-field* props]
(when has-hint
[:> hint-message* {:id id
:class hint-class
:message hint-message
:type hint-type}])]))

View File

@@ -12,3 +12,7 @@
gap: var(--sp-xs);
inline-size: 100%;
}
.hint-formated {
white-space: pre;
}

View File

@@ -118,6 +118,7 @@
(mf/use-fn
(mf/deps disabled)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not disabled
(swap! is-open* not))))

View File

@@ -53,10 +53,15 @@
"true")
:aria-describedby (when has-hint
(str id "-hint"))
:aria-labelledby tooltip-id
:type (d/nilv type "text")
:id id
:max-length (d/nilv max-length max-input-length)})
props (if (and aria-label (not (some? icon)))
(mf/spread-props props
{:aria-label aria-label})
(mf/spread-props props
{:aria-labelledby tooltip-id}))
inside-class (stl/css-case :input-wrapper true
:has-hint has-hint
:hint-type-hint (= hint-type "hint")

View File

@@ -300,6 +300,7 @@
"A collection of all icons"
(collect-icons))
(def ^:private ^:const icon-size-l 32)
(def ^:private ^:const icon-size-m 16)
(def ^:private ^:const icon-size-s 12)
@@ -308,7 +309,7 @@
[:class {:optional true} [:maybe :string]]
[:icon-id [:and :string [:fn #(contains? icon-list %)]]]
[:size {:optional true}
[:maybe [:enum "s" "m"]]]])
[:maybe [:enum "s" "m" "l"]]]])
(mf/defc icon*
{::mf/schema schema:icon}
@@ -317,10 +318,14 @@
{:class [class (stl/css :icon)]
:width icon-size-m
:height icon-size-m})
size-px (if (= size "s")
icon-size-s
icon-size-m)
offset (/ (- icon-size-m size-px) 2)]
size-px (cond (= size "l") icon-size-l
(= size "s") icon-size-s
:else icon-size-m)
offset (if (or (= size "s") (= size "m"))
(/ (- icon-size-m size-px) 2)
0)]
[:> :svg props
[:use {:href (dm/str "#icon-" icon-id)

View File

@@ -15,7 +15,7 @@
}
.swatch {
--border-color: var(--color-background-quaternary);
--border-color: var(--color-accent-primary-muted);
--border-radius: #{$br-4};
--border-color-active: var(--color-foreground-primary);
--border-color-active-inset: var(--color-background-primary);

View File

@@ -34,10 +34,9 @@
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value true))))
props
(mf/spread-props props {:on-change on-change
:default-value value})
:value value})
props
(if (and error touched?)

View File

@@ -78,3 +78,9 @@
:settings-notifications
[:& notifications-page* {:profile profile}])]]]]))
(mf/defc settings-page*
{::mf/lazy-load true}
[props]
[:> settings* props])

View File

@@ -624,7 +624,6 @@
;; --- Component: Viewer
(mf/defc viewer*
{::mf/props :obj}
[{:keys [file-id share-id page-id] :as props}]
(mf/with-effect [file-id page-id share-id]
(let [params {:file-id file-id
@@ -643,3 +642,8 @@
[:> loader* {:title (tr "labels.loading")
:overlay true}]))
(mf/defc viewer-page*
{::mf/lazy-load true}
[props]
[:> viewer* props])

View File

@@ -165,7 +165,7 @@
(dsh/lookup-page state file-id page-id))))
st/state))
(mf/defc workspace-page*
(mf/defc workspace-inner*
{::mf/private true}
[{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id]
@@ -252,10 +252,16 @@
:touch-action "none"}}
[:> context-menu*]
(if (and file-loaded? page-id)
[:> workspace-page*
[:> workspace-inner*
{:page-id page-id
:file-id file-id
:file file
:wglobal wglobal
:layout layout}]
[:> workspace-loader*])]]]]]]))
(mf/defc workspace-page*
{::mf/lazy-load true}
[props]
[:> workspace* props])

View File

@@ -418,11 +418,11 @@
[:& radio-buttons {:selected color-style
:on-change toggle-token-color
:name "color-style"}
[:& radio-button {:icon deprecated-icon/swatches
[:& radio-button {:icon i/swatches
:value :direct-color
:title (tr "labels.color")
:id "opt-color"}]
[:& radio-button {:icon deprecated-icon/tokens
[:& radio-button {:icon i/tokens
:value :token-color
:title (tr "workspace.colorpicker.color-tokens")
:id "opt-token-color"}]])]

View File

@@ -11,7 +11,6 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.path-names :as cpn]
[app.common.spec :as us]
[app.common.thumbnails :as thc]
[app.common.types.component :as ctk]
[app.common.types.container :as ctn]
@@ -38,7 +37,6 @@
[app.util.i18n :as i18n :refer [c tr]]
[app.util.strings :refer [matches-search]]
[app.util.timers :as ts]
[cljs.spec.alpha :as s]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@@ -97,10 +95,6 @@
(str (str/slice (:path asset) (count path)))
(cpn/merge-path-item (:name asset))))
(s/def ::asset-name ::us/not-empty-string)
(s/def ::name-group-form
(s/keys :req-un [::asset-name]))
(def initial-context-menu-state
{:open? false :top nil :left nil})

View File

@@ -28,7 +28,6 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.assets.groups :as grp]
[app.util.dom :as dom]
@@ -567,11 +566,11 @@
[:& radio-buttons {:selected (if is-listing-thumbs "grid" "list")
:on-change toggle-list-style
:name "listing-style"}
[:& radio-button {:icon deprecated-icon/view-as-list
[:& radio-button {:icon i/view-as-list
:value "list"
:title (tr "workspace.assets.list-view")
:id "opt-list"}]
[:& radio-button {:icon deprecated-icon/flex-grid
[:& radio-button {:icon i/flex-grid
:value "grid"
:title (tr "workspace.assets.grid-view")
:id "opt-grid"}]]])

View File

@@ -25,7 +25,7 @@
[app.main.ui.workspace.sidebar.options.menus.bool :refer [bool-options*]]
[app.main.ui.workspace.sidebar.options.menus.component :refer [component-menu*]]
[app.main.ui.workspace.sidebar.options.menus.grid-cell :as grid-cell]
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu]]
[app.main.ui.workspace.sidebar.options.menus.interactions :refer [interactions-menu*]]
[app.main.ui.workspace.sidebar.options.menus.layout-container :as layout-container]
[app.main.ui.workspace.sidebar.options.page :as page]
[app.main.ui.workspace.sidebar.options.shapes.bool :as bool]
@@ -215,7 +215,7 @@
(case options-mode
:prototype
[:div {:class (stl/css :element-options :interaction-options)}
[:& interactions-menu {:shape (first shapes)}]]
[:> interactions-menu* {:shape (first shapes)}]]
:inspect
[:div {:class (stl/css :element-options :inspect-options)}

View File

@@ -13,6 +13,7 @@
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -99,10 +100,10 @@
:name "frame-orientation"
:wide true
:class (stl/css :radio-buttons)}
[:& radio-button {:icon deprecated-icon/size-vertical
[:& radio-button {:icon i/size-vertical
:value "vertical"
:id "size-vertical"}]
[:& radio-button {:icon deprecated-icon/size-horizontal
[:& radio-button {:icon i/size-horizontal
:value "horizontal"
:id "size-horizontal"}]]]))

View File

@@ -16,6 +16,7 @@
[app.main.features :as features]
[app.main.store :as st]
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
@@ -78,22 +79,22 @@
:class (stl/css :boolean-radio-btn)
:on-change on-change
:name "bool-options"}
[:& radio-button {:icon deprecated-icon/boolean-union
[:& radio-button {:icon i/boolean-union
:value "union"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.union") " (" (sc/get-tooltip :bool-union) ")")
:id "bool-opt-union"}]
[:& radio-button {:icon deprecated-icon/boolean-difference
[:& radio-button {:icon i/boolean-difference
:value "difference"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.difference") " (" (sc/get-tooltip :bool-difference) ")")
:id "bool-opt-differente"}]
[:& radio-button {:icon deprecated-icon/boolean-intersection
[:& radio-button {:icon i/boolean-intersection
:value "intersection"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.intersection") " (" (sc/get-tooltip :bool-intersection) ")")
:id "bool-opt-intersection"}]
[:& radio-button {:icon deprecated-icon/boolean-exclude
[:& radio-button {:icon i/boolean-exclude
:value "exclude"
:disabled disabled-bool-btns
:title (str (tr "workspace.shape.menu.exclude") " (" (sc/get-tooltip :bool-exclude) ")")

View File

@@ -42,7 +42,6 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.product.input-with-meta :refer [input-with-meta*]]
[app.main.ui.hooks :as h]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.main.ui.workspace.sidebar.options.menus.variants-help-modal]
[app.util.debug :as dbg]
@@ -798,10 +797,10 @@
[:& radio-buttons {:selected (if (:listing-thumbs? filters) "grid" "list")
:on-change toggle-list-style
:name "swap-listing-style"}
[:& radio-button {:icon deprecated-icon/view-as-list
[:& radio-button {:icon i/view-as-list
:value "list"
:id "swap-opt-list"}]
[:& radio-button {:icon deprecated-icon/flex-grid
[:& radio-button {:icon i/flex-grid
:value "grid"
:id "swap-opt-grid"}]]]

View File

@@ -206,7 +206,7 @@
:data-value "bottom"
:on-click on-constraint-button-clicked}
[:span {:class (stl/css :resalted-area)}]]]]
[:div {:class (stl/css :contraints-selects)}
[:div {:class (stl/css :constraints-selects)}
[:div {:class (stl/css :horizontal-select) :data-testid "constraint-h-select"}
[:& select
{:default-value (if (not= constraints-h :multiple) (d/nilv (d/name constraints-h) "scale") "")

View File

@@ -120,7 +120,9 @@
}
.constraints-selects {
@include deprecated.flexColumn;
display: flex;
flex-direction: column;
gap: 4px;
}
.horizontal-select,

View File

@@ -18,6 +18,7 @@
[app.main.ui.components.numeric-input :refer [numeric-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.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
@@ -52,29 +53,29 @@
:name (dm/str "flex-align-items-" type)}
[:& radio-button {:value "start"
:icon (if is-col?
deprecated-icon/align-self-row-left
deprecated-icon/align-self-column-top)
i/align-self-row-left
i/align-self-column-top)
:title "Align self start"
:id (dm/str "align-self-start-" type)}]
[:& radio-button {:value "center"
:icon (if is-col?
deprecated-icon/align-self-row-center
deprecated-icon/align-self-column-center)
i/align-self-row-center
i/align-self-column-center)
:title "Align self center"
:id (dm/str "align-self-center-" type)}]
[:& radio-button {:value "end"
:icon (if is-col?
deprecated-icon/align-self-row-right
deprecated-icon/align-self-column-bottom)
i/align-self-row-right
i/align-self-column-bottom)
:title "Align self end"
:id (dm/str "align-self-end-" type)}]
[:& radio-button {:value "stretch"
:icon (if is-col?
deprecated-icon/align-self-row-stretch
deprecated-icon/align-self-column-stretch)
i/align-self-row-stretch
i/align-self-column-stretch)
:title "Align self stretch"
:id (dm/str "align-self-stretch-" type)}]]]))

View File

File diff suppressed because it is too large Load Diff

View File

@@ -4,22 +4,51 @@
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
@use "ds/mixins.scss" as *;
@use "ds/typography.scss" as t;
@use "../../../sidebar/common/sidebar.scss" as sidebar;
.interactions-content {
.wrapper {
display: flex;
flex-direction: column;
gap: deprecated.$s-8;
padding-left: var(--sp-m);
gap: var(--sp-s);
padding-inline-start: var(--sp-m);
}
.interaction-options {
@include deprecated.flexColumn;
.section {
@include sidebar.option-grid-structure;
}
.help-content {
padding: deprecated.$s-32 0;
width: deprecated.$s-200;
.title {
grid-column: span 8;
}
.title-bar {
padding-inline-start: var(--sp-xxs);
}
.content {
grid-column: span 8;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
margin-block-start: var(--sp-xs);
}
.content-interactions {
gap: var(--sp-l);
}
.help {
display: flex;
flex-direction: column;
gap: var(--sp-xxxl);
inline-size: $sz-200;
padding: var(--sp-xxxl) 0;
margin: 0 auto;
}
@@ -27,130 +56,168 @@
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: deprecated.$s-40;
gap: deprecated.$s-12;
gap: var(--sp-m);
}
.interactions-help-icon {
@include deprecated.flexCenter;
width: deprecated.$s-48;
height: deprecated.$s-48;
border-radius: deprecated.$br-circle;
background-color: var(--pill-background-color);
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
height: deprecated.$s-32;
width: deprecated.$s-32;
.help-text {
@include t.use-typography("body-small");
text-align: center;
color: var(--color-foreground-secondary);
}
.help-icon {
display: flex;
justify-content: center;
align-items: center;
inline-size: $sz-48;
block-size: $sz-48;
}
.help-icon-inner {
color: var(--color-foreground-secondary);
inline-size: $sz-32;
block-size: $sz-32;
}
.interaction-item {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.prototype-pill {
display: flex;
align-items: center;
gap: px2rem(1);
border-radius: $br-8;
padding: var(--sp-s) var(--sp-m);
block-size: $sz-32;
padding: 0;
&.double {
block-size: $sz-48;
.prototype-pill-button {
block-size: $sz-48;
}
}
&:has(.prototype-pill-input:focus) {
outline: $b-1 solid var(--color-accent-primary);
}
}
.after {
@include deprecated.bodySmallTypography;
margin-top: deprecated.$s-1;
.prototype-pill-button {
&.left {
border-end-end-radius: 0;
border-start-end-radius: 0;
}
&.right {
border-start-start-radius: 0;
border-end-start-radius: 0;
}
}
.interactions-help {
@include deprecated.bodySmallTypography;
text-align: center;
color: var(--title-foreground-color);
}
.element-set {
@include deprecated.flexColumn;
}
.interactions-info {
.prototype-pill-main {
display: flex;
flex-grow: 1;
display: grid;
block-size: 100%;
inline-size: 100%;
}
.trigger-name {
.prototype-pill-center {
flex-grow: 1;
display: flex;
align-items: center;
block-size: 100%;
padding: 0 var(--sp-s);
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
}
.action-summary {
color: var(--color-foreground-secondary);
@include deprecated.textEllipsis;
}
.groups {
@include deprecated.flexColumn(deprecated.$s-12);
}
.element-set-options-group-open {
@include deprecated.flexColumn;
}
.extended-options {
@include deprecated.flexColumn;
}
.property-list {
list-style: none;
margin: 0;
.prototype-pill-info {
display: grid;
row-gap: deprecated.$s-16;
margin-block: calc(#{deprecated.$s-16} - #{deprecated.$s-4});
inline-size: 100%;
}
.property-row {
@extend .attr-row;
height: auto;
&.big-row {
height: 100%;
}
.interaction-name {
@include deprecated.twoLineTextEllipsis;
@include deprecated.bodySmallTypography;
padding-left: deprecated.$s-4;
width: deprecated.$s-92;
margin: auto 0;
grid-area: name;
color: var(--title-foreground-color);
}
.select-wrapper {
display: flex;
align-items: center;
grid-area: content;
.easing-select {
width: deprecated.$s-156;
padding: 0 deprecated.$s-8;
.dropdown-upwards {
bottom: deprecated.$s-36;
width: deprecated.$s-156;
top: unset;
}
.prototype-pill-input {
@include t.use-typography("body-small");
border: none;
background: none;
outline: none;
block-size: 100%;
inline-size: 100%;
flex-grow: 1;
margin: var(--sp-xxs) 0;
padding: 0 0 0 var(--sp-s);
margin: 0;
background-color: var(--color-background-tertiary);
color: var(--color-foreground-primary);
display: grid;
inline-size: 100%;
&:hover {
background-color: var(--color-background-quaternary);
&:active {
background-color: var(--color-background-quaternary);
}
}
.input-element-wrapper {
@extend .input-element;
@include deprecated.bodySmallTypography;
grid-area: content;
}
.buttons-wrapper {
grid-area: content;
.right svg {
transform: rotate(-90deg);
}
.left svg {
transform: rotate(90deg);
}
.up svg {
transform: rotate(180deg);
}
}
.inputs-wrapper {
grid-area: content;
@include deprecated.flexRow;
.radio-btn {
@extend .input-checkbox;
}
&:focus {
background-color: var(--color-background-tertiary);
}
}
.position-btns-wrapper {
grid-area: content;
.prototype-pill-name {
@include t.use-typography("body-small");
@include textEllipsis;
color: var(--color-foreground-primary);
}
.prototype-pill-description {
@include t.use-typography("body-small");
@include textEllipsis;
color: var(--color-foreground-secondary);
}
.interaction-row {
@include sidebar.option-grid-structure;
}
.interaction-row-label {
grid-column: span 3;
display: flex;
align-items: center;
color: var(--color-foreground-secondary);
}
.interaction-row-name {
@include twoLineTextEllipsis;
@include t.use-typography("body-small");
color: var(--color-foreground-secondary);
}
.interaction-row-select {
grid-column: span 5;
}
.interaction-row-checkbox {
grid-column: 4 / span 5;
min-block-size: $sz-32;
display: flex;
align-items: center;
}
.interaction-row-input {
grid-column: span 5;
}
.interaction-row-radio {
grid-column: 4 / span 5;
}
.interaction-row-position {
grid-column: 4 / span 5;
display: grid;
grid-template-areas:
"topleft top topright"
@@ -158,191 +225,30 @@
"bottomleft bottom bottomright";
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
width: deprecated.$s-84;
height: deprecated.$s-84;
border-radius: deprecated.$br-8;
inline-size: calc($sz-32 * 3);
block-size: calc($sz-32 * 3);
border-radius: $br-8;
background-color: var(--color-background-tertiary);
.center-btn {
.center {
grid-area: center;
}
.top-left-btn {
.top-left {
grid-area: topleft;
}
.top-right-btn {
grid-area: topright;
}
.top-center-btn {
.top-center {
grid-area: top;
}
.bottom-left-btn {
.top-right {
grid-area: topright;
}
.bottom-left {
grid-area: bottomleft;
}
.bottom-right-btn {
grid-area: bottomright;
}
.bottom-center-btn {
.bottom-center {
grid-area: bottom;
}
}
.direction-btn {
@extend .button-tertiary;
height: deprecated.$s-28;
width: deprecated.$s-28;
&.active {
@extend .button-icon-selected;
}
}
.checkbox-option {
@extend .input-checkbox;
grid-area: content;
line-height: 1.2;
label {
align-items: start;
}
}
.interactions-summary {
@extend .asset-element;
height: deprecated.$s-44;
padding: 0;
gap: deprecated.$s-8;
.remove-btn {
@extend .button-tertiary;
height: deprecated.$s-32;
width: deprecated.$s-28;
svg {
@extend .button-icon-small;
}
}
}
.extend-btn {
@extend .button-tertiary;
--button-tertiary-border-width: var(--expand-button-icon-border-width);
height: 100%;
width: deprecated.$s-28;
border-end-end-radius: 0;
border-start-end-radius: 0;
padding: 0;
svg {
@extend .button-icon;
}
position: relative;
&:after {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
border-inline-end: deprecated.$s-1 solid var(--panel-background-color);
}
&.extended {
@extend .button-icon-selected;
--button-tertiary-border-width: var(--expand-button-icon-border-width-selected);
}
}
.corner-icon {
fill: none;
stroke: currentColor;
width: deprecated.$s-12;
height: deprecated.$s-12;
}
.flow-element {
@include deprecated.flexRow;
}
.flow-info {
display: flex;
align-items: center;
gap: deprecated.$s-2;
border-radius: deprecated.$s-8;
background-color: var(--input-details-color);
height: deprecated.$s-32;
width: 100%;
flex-grow: 1;
}
.flow-name-wrapper {
@include deprecated.bodySmallTypography;
@include deprecated.focusInput;
display: flex;
align-items: center;
gap: deprecated.$s-4;
flex-grow: 1;
height: deprecated.$s-32;
width: 100%;
border-radius: deprecated.$br-8;
padding: 0;
margin-right: 0;
background-color: var(--input-background-color);
border: deprecated.$s-1 solid var(--input-border-color);
color: var(--input-foreground-color);
.start-flow-btn {
@include deprecated.buttonStyle;
height: deprecated.$s-32;
width: deprecated.$s-28;
padding: 0 deprecated.$s-2 0 deprecated.$s-8;
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
background-color: transparent;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
&:hover {
stroke: var(--input-foreground-color-active);
}
}
}
.flow-input {
@extend .input-base;
@include deprecated.bodySmallTypography;
background-color: transparent;
height: deprecated.$s-28;
}
.flow-input-wrapper {
@include deprecated.bodySmallTypography;
display: flex;
align-items: center;
height: deprecated.$s-28;
padding: 0;
width: 100%;
margin: 0;
flex-grow: 1;
background-color: transparent;
color: var(--input-foreground-color);
border-radius: deprecated.$br-8;
}
&:hover {
background-color: var(--input-background-color-hover);
border: deprecated.$s-1 solid var(--input-border-color-hover);
&:active {
background-color: var(--input-background-color-hover);
.flow-input-wrapper {
background-color: var(--input-background-color-hover);
}
}
}
&:focus,
&:focus-within {
background-color: var(--input-background-color-focus);
border: deprecated.$s-1 solid var(--input-border-color-focus);
&:hover {
border: deprecated.$s-1 solid var(--input-border-color-focus);
}
}
&.editing {
background-color: var(--input-background-color-active);
border: deprecated.$s-1 solid var(--input-border-color-active);
.bottom-right {
grid-area: bottomright;
}
}

View File

@@ -42,10 +42,10 @@
(defn- dir-icons-refactor
[val]
(case val
:row deprecated-icon/grid-row
:row-reverse deprecated-icon/row-reverse
:column deprecated-icon/column
:column-reverse deprecated-icon/column-reverse))
:row i/grid-row
:row-reverse i/row-reverse
:column i/column
:column-reverse i/column-reverse))
(mf/defc numeric-input-wrapper*
@@ -111,63 +111,63 @@
:align-items
(if column?
(case val
:start deprecated-icon/align-items-column-start
:end deprecated-icon/align-items-column-end
:center deprecated-icon/align-items-column-center)
:start i/align-items-column-start
:end i/align-items-column-end
:center i/align-items-column-center)
(case val
:start deprecated-icon/align-items-row-start
:end deprecated-icon/align-items-row-end
:center deprecated-icon/align-items-row-center))
:start i/align-items-row-start
:end i/align-items-row-end
:center i/align-items-row-center))
:justify-content
(if column?
(case val
:start deprecated-icon/justify-content-column-start
:end deprecated-icon/justify-content-column-end
:center deprecated-icon/justify-content-column-center
:space-around deprecated-icon/justify-content-column-around
:space-evenly deprecated-icon/justify-content-column-evenly
:space-between deprecated-icon/justify-content-column-between)
:start i/justify-content-column-start
:end i/justify-content-column-end
:center i/justify-content-column-center
:space-around i/justify-content-column-around
:space-evenly i/justify-content-column-evenly
:space-between i/justify-content-column-between)
(case val
:start deprecated-icon/justify-content-row-start
:end deprecated-icon/justify-content-row-end
:center deprecated-icon/justify-content-row-center
:space-around deprecated-icon/justify-content-row-around
:space-evenly deprecated-icon/justify-content-row-evenly
:space-between deprecated-icon/justify-content-row-between))
:start i/justify-content-row-start
:end i/justify-content-row-end
:center i/justify-content-row-center
:space-around i/justify-content-row-around
:space-evenly i/justify-content-row-evenly
:space-between i/justify-content-row-between))
:align-content
(if column?
(case val
:start deprecated-icon/align-content-column-start
:end deprecated-icon/align-content-column-end
:center deprecated-icon/align-content-column-center
:space-around deprecated-icon/align-content-column-around
:space-evenly deprecated-icon/align-content-column-evenly
:space-between deprecated-icon/align-content-column-between
:start i/align-content-column-start
:end i/align-content-column-end
:center i/align-content-column-center
:space-around i/align-content-column-around
:space-evenly i/align-content-column-evenly
:space-between i/align-content-column-between
:stretch nil)
(case val
:start deprecated-icon/align-content-row-start
:end deprecated-icon/align-content-row-end
:center deprecated-icon/align-content-row-center
:space-around deprecated-icon/align-content-row-around
:space-evenly deprecated-icon/align-content-row-evenly
:space-between deprecated-icon/align-content-row-between
:start i/align-content-row-start
:end i/align-content-row-end
:center i/align-content-row-center
:space-around i/align-content-row-around
:space-evenly i/align-content-row-evenly
:space-between i/align-content-row-between
:stretch nil))
:align-self
(if column?
(case val
:auto deprecated-icon/remove-icon
:start deprecated-icon/align-self-row-left
:end deprecated-icon/align-self-row-right
:center deprecated-icon/align-self-row-center)
:auto i/remove
:start i/align-self-row-left
:end i/align-self-row-right
:center i/align-self-row-center)
(case val
:auto deprecated-icon/remove-icon
:start deprecated-icon/align-self-column-top
:end deprecated-icon/align-self-column-bottom
:center deprecated-icon/align-self-column-center))))
:auto i/remove
:start i/align-self-column-top
:end i/align-self-column-bottom
:center i/align-self-column-center))))
(defn get-layout-grid-icon
[type val ^boolean column?]
@@ -175,32 +175,32 @@
:align-items
(if column?
(case val
:auto deprecated-icon/remove-icon
:start deprecated-icon/align-self-row-left
:end deprecated-icon/align-self-row-right
:center deprecated-icon/align-self-row-center)
:auto i/remove
:start i/align-self-row-left
:end i/align-self-row-right
:center i/align-self-row-center)
(case val
:auto deprecated-icon/remove-icon
:start deprecated-icon/align-self-column-top
:end deprecated-icon/align-self-column-bottom
:center deprecated-icon/align-self-column-center))
:auto i/remove
:start i/align-self-column-top
:end i/align-self-column-bottom
:center i/align-self-column-center))
:justify-items
(if (not column?)
(case val
:start deprecated-icon/align-content-column-start
:center deprecated-icon/align-content-column-center
:end deprecated-icon/align-content-column-end
:space-around deprecated-icon/align-content-column-around
:space-between deprecated-icon/align-content-column-between
:stretch deprecated-icon/align-content-column-stretch)
:start i/align-content-column-start
:center i/align-content-column-center
:end i/align-content-column-end
:space-around i/align-content-column-around
:space-between i/align-content-column-between
:stretch i/align-content-column-stretch)
(case val
:start deprecated-icon/align-content-row-start
:center deprecated-icon/align-content-row-center
:end deprecated-icon/align-content-row-end
:space-around deprecated-icon/align-content-row-around
:space-between deprecated-icon/align-content-row-between
:stretch deprecated-icon/align-content-row-stretch))))
:start i/align-content-row-start
:center i/align-content-row-center
:end i/align-content-row-end
:space-around i/align-content-row-around
:space-between i/align-content-row-between
:stretch i/align-content-row-stretch))))
(mf/defc direction-row-flex
{::mf/props :obj

View File

@@ -455,10 +455,10 @@
:name "frame-orientation"
:wide true
:class (stl/css :radio-buttons)}
[:& radio-button {:icon deprecated-icon/size-vertical
[:& radio-button {:icon i/size-vertical
:value "vert"
:id "size-vertical"}]
[:& radio-button {:icon deprecated-icon/size-horizontal
[:& radio-button {:icon i/size-horizontal
:value "horiz"
:id "size-horizontal"}]]
[:> icon-button*

View File

@@ -53,19 +53,19 @@
[:& radio-button {:value "left"
:id "text-align-left"
:title (tr "workspace.options.text-options.text-align-left")
:icon deprecated-icon/text-align-left}]
:icon i/text-align-left}]
[:& radio-button {:value "center"
:id "text-align-center"
:title (tr "workspace.options.text-options.text-align-center")
:icon deprecated-icon/text-align-center}]
:icon i/text-align-center}]
[:& radio-button {:value "right"
:id "text-align-right"
:title (tr "workspace.options.text-options.text-align-right")
:icon deprecated-icon/text-align-right}]
:icon i/text-align-right}]
[:& radio-button {:value "justify"
:id "text-align-justify"
:title (tr "workspace.options.text-options.text-align-justify")
:icon deprecated-icon/text-justify}]]]))
:icon i/text-justify}]]]))
(mf/defc text-direction-options
[{:keys [values on-change on-blur] :as props}]
@@ -88,12 +88,12 @@
:type "checkbox"
:id "ltr-text-direction"
:title (tr "workspace.options.text-options.direction-ltr")
:icon deprecated-icon/text-ltr}]
:icon i/text-ltr}]
[:& radio-button {:value "rtl"
:type "checkbox"
:id "rtl-text-direction"
:title (tr "workspace.options.text-options.direction-rtl")
:icon deprecated-icon/text-rtl}]]]))
:icon i/text-rtl}]]]))
(mf/defc vertical-align
[{:keys [values on-change on-blur] :as props}]
@@ -113,15 +113,15 @@
[:& radio-button {:value "top"
:id "vertical-text-align-top"
:title (tr "workspace.options.text-options.align-top")
:icon deprecated-icon/text-top}]
:icon i/text-top}]
[:& radio-button {:value "center"
:id "vertical-text-align-center"
:title (tr "workspace.options.text-options.align-middle")
:icon deprecated-icon/text-middle}]
:icon i/text-middle}]
[:& radio-button {:value "bottom"
:id "vertical-text-align-bottom"
:title (tr "workspace.options.text-options.align-bottom")
:icon deprecated-icon/text-bottom}]]]))
:icon i/text-bottom}]]]))
(mf/defc grow-options
[{:keys [ids values on-blur] :as props}]
@@ -150,15 +150,15 @@
[:& radio-button {:value "fixed"
:id "text-fixed-grow"
:title (tr "workspace.options.text-options.grow-fixed")
:icon deprecated-icon/text-fixed}]
:icon i/text-fixed}]
[:& radio-button {:value "auto-width"
:id "text-auto-width-grow"
:title (tr "workspace.options.text-options.grow-auto-width")
:icon deprecated-icon/text-auto-width}]
:icon i/text-auto-width}]
[:& radio-button {:value "auto-height"
:id "text-auto-height-grow"
:title (tr "workspace.options.text-options.grow-auto-height")
:icon deprecated-icon/text-auto-height}]]]))
:icon i/text-auto-height}]]]))
(mf/defc text-decoration-options
[{:keys [values on-change on-blur] :as props}]
@@ -180,12 +180,12 @@
:type "checkbox"
:id "underline-text-decoration"
:title (tr "workspace.options.text-options.underline" (sc/get-tooltip :underline))
:icon deprecated-icon/text-underlined}]
:icon i/text-underlined}]
[:& radio-button {:value "line-through"
:type "checkbox"
:id "line-through-text-decoration"
:title (tr "workspace.options.text-options.strikethrough" (sc/get-tooltip :line-through))
:icon deprecated-icon/text-stroked}]]]))
:icon i/text-stroked}]]]))
(mf/defc text-menu
{::mf/wrap [mf/memo]}

View File

@@ -26,6 +26,7 @@
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.context :as ctx]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
@@ -418,17 +419,17 @@
[:& radio-buttons {:selected text-transform
:on-change handle-change
:name "text-transform"}
[:& radio-button {:icon deprecated-icon/text-uppercase
[:& radio-button {:icon i/text-uppercase
:type "checkbox"
:title (tr "inspect.attributes.typography.text-transform.uppercase")
:value "uppercase"
:id "text-transform-uppercase"}]
[:& radio-button {:icon deprecated-icon/text-mixed
[:& radio-button {:icon i/text-mixed
:type "checkbox"
:value "capitalize"
:title (tr "inspect.attributes.typography.text-transform.capitalize")
:id "text-transform-capitalize"}]
[:& radio-button {:icon deprecated-icon/text-lowercase
[:& radio-button {:icon i/text-lowercase
:type "checkbox"
:title (tr "inspect.attributes.typography.text-transform.lowercase")
:value "lowercase"

View File

@@ -23,7 +23,7 @@
[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.main.ui.workspace.tokens.management.create.input-token :refer [input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@@ -52,8 +52,6 @@
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
@@ -82,7 +80,7 @@
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [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.
@@ -170,7 +168,9 @@
[: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)]
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
@@ -187,7 +187,7 @@
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-input-token*
[:> input-token*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value

View File

@@ -23,7 +23,7 @@
[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.main.ui.workspace.tokens.management.create.color-input-token :refer [color-input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@@ -51,8 +51,7 @@
#(not (cft/token-name-path-exists? % tokens-tree))]]]
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]
[:color-result {:optional true} ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
@@ -82,7 +81,7 @@
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [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.
@@ -170,7 +169,9 @@
[: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)]
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
@@ -187,7 +188,7 @@
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-color-input-token*
[:> color-input-token*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value

View File

@@ -0,0 +1,435 @@
;; 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-input-token
(:require-macros [app.main.style :as stl])
(:require
[app.common.colors :as color]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.color :as cl]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.refs :as refs]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- resolve-value
[tokens prev-token value]
(let [token
{:value value
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
(defn- hex->color-obj
[hex]
(when-let [tc (tinycolor/valid-color hex)]
(let [hex (tinycolor/->hex-string tc)
alpha (tinycolor/alpha tc)
[r g b] (cl/hex->rgb hex)
[h s v] (cl/hex->hsv hex)]
{:hex hex
:r r :g g :b b
:h h :s s :v v
:alpha alpha})))
(mf/defc ramp*
[{:keys [color on-change]}]
(let [wrapper-node-ref (mf/use-ref nil)
dragging-ref (mf/use-ref false)
on-start-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref true))
on-finish-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref false))
internal-color*
(mf/use-state #(hex->color-obj color))
internal-color
(deref internal-color*)
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [{:keys [hex alpha] :as selector-color}]
(let [dragging? (mf/ref-val dragging-ref)]
(when-not (and dragging? hex)
(reset! internal-color* selector-color)
(on-change hex alpha)))))]
(mf/use-effect
(mf/deps color)
(fn []
;; Update internal color when user changes input value
(when-let [color (tinycolor/valid-color color)]
(when-not (= (tinycolor/->hex-string color) (:hex internal-color))
(reset! internal-color* (hex->color-obj color))))))
(colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color)
[:div {:ref wrapper-node-ref}
[:> ramp-selector*
{:color internal-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag
:on-change on-change'}]]))
(mf/defc color-input-token*
[{:keys [name tokens token] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
touched?
(and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error
(get-in @form [:errors input-name])
value
(get-in @form [:data input-name] "")
color-resolved
(get-in @form [:data :color-result] "")
valid-color (or (tinycolor/valid-color value)
(tinycolor/valid-color color-resolved))
profile (mf/deref refs/profile)
default-bullet-color
(case (:theme profile)
"light"
color/background-quaternary-light
color/background-quaternary)
hex
(if valid-color
(tinycolor/->hex-string (tinycolor/valid-color valid-color))
default-bullet-color)
alpha
(if (tinycolor/valid-color valid-color)
(tinycolor/alpha (tinycolor/valid-color valid-color))
1)
resolve-stream
(mf/with-memo [token]
(if-let [value (:value token)]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
on-click-swatch
(mf/use-fn
(mf/deps color-ramp-open?)
(fn []
(let [open? (not color-ramp-open?)]
(reset! color-ramp-open* open?))))
swatch
(mf/html
[:> swatch*
{:background {:color hex :opacity alpha}
:show-tooltip false
:data-testid "token-form-color-bullet"
:class (stl/css :slot-start)
:on-click on-click-swatch}])
on-change-value
(mf/use-fn
(mf/deps resolve-stream input-name value)
(fn [hex alpha]
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
prev-input-color (some-> value
(tinycolor/valid-color))
;; If the input is a reference we will take the format from the computed value
prev-computed-color (when-not prev-input-color
(some-> value (tinycolor/valid-color)))
prev-format (some-> (or prev-input-color prev-computed-color)
(tinycolor/color-format))
to-rgba? (and
(< alpha 1)
(or (= prev-format "hex") (not prev-format)))
to-hex? (and (not prev-format) (= alpha 1))
format (cond
to-rgba? "rgba"
to-hex? "hex"
prev-format prev-format
:else "hex")
color-value (-> (tinycolor/valid-color hex)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(when (not= value color-value)
(fm/on-input-change form input-name color-value true)
(rx/push! resolve-stream color-value)))))
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [raw-value (-> event dom/get-target dom/get-input-value)
value (if (tinycolor/hex-without-hash-prefix? raw-value)
(dm/str "#" raw-value)
raw-value)]
(fm/on-input-change form input-name value true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
;; TODO: Review this value vs default-value
:value (or value "")
:hint-message (:message hint)
:variant "comfortable"
:slot-start swatch
:hint-type (:type hint)})
props
(if (and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs! (fn [{:keys [error value]}]
(let [touched? (get-in @form [:touched input-name])]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(swap! form assoc-in [:data :color-result] "")
(reset! hint* {:message error :type "error"}))
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))]
(swap! form update :extra-errors dissoc input-name)
(swap! form assoc-in [:data :color-result] value)
(reset! hint* {:message message :type "hint"}))))))))]
(fn []
(rx/dispose! subs))))
[:*
[:> input* props]
(when color-ramp-open?
[:> ramp* {:color value :on-change on-change-value}])]))
(defn- on-composite-indexed-input-token-change
([form field index value composite-type]
(on-composite-indexed-input-token-change form field index value composite-type false))
([form field index value composite-type trim?]
(letfn [(clean-errors [errors]
(-> errors
(dissoc field)
(not-empty)))]
(swap! form (fn [state]
(-> state
(assoc-in [:data :value composite-type index field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc color-input-token-indexed*
[{:keys [name tokens token index composite-type] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
error
(get-in @form [:errors :value composite-type index input-name])
value
(get-in @form [:data :value composite-type index input-name] "")
color-resolved
(get-in @form [:data :value composite-type index :color-result] "")
valid-color (or (tinycolor/valid-color value)
(tinycolor/valid-color color-resolved))
profile (mf/deref refs/profile)
default-bullet-color
(case (:theme profile)
"light"
color/background-quaternary-light
color/background-quaternary)
hex
(if valid-color
(tinycolor/->hex-string (tinycolor/valid-color valid-color))
default-bullet-color)
alpha
(if (tinycolor/valid-color valid-color)
(tinycolor/alpha (tinycolor/valid-color valid-color))
1)
resolve-stream
(mf/with-memo [token]
(if-let [value (get-in token [:value composite-type index input-name])]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
on-click-swatch
(mf/use-fn
(mf/deps color-ramp-open?)
(fn []
(let [open? (not color-ramp-open?)]
(reset! color-ramp-open* open?))))
swatch
(mf/html
[:> swatch*
{:background {:color hex :opacity alpha}
:show-tooltip false
:data-testid "token-form-color-bullet"
:class (stl/css :slot-start)
:on-click on-click-swatch}])
on-change-value
(mf/use-fn
(mf/deps resolve-stream input-name value index)
(fn [hex alpha]
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
prev-input-color (some-> value
(tinycolor/valid-color))
;; If the input is a reference we will take the format from the computed value
prev-computed-color (when-not prev-input-color
(some-> value (tinycolor/valid-color)))
prev-format (some-> (or prev-input-color prev-computed-color)
(tinycolor/color-format))
to-rgba? (and
(< alpha 1)
(or (= prev-format "hex") (not prev-format)))
to-hex? (and (not prev-format) (= alpha 1))
format (cond
to-rgba? "rgba"
to-hex? "hex"
prev-format prev-format
:else "hex")
color-value (-> (tinycolor/valid-color hex)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(when (not= value color-value)
(on-composite-indexed-input-token-change form input-name index color-value composite-type true)
(rx/push! resolve-stream color-value)))))
on-change
(mf/use-fn
(mf/deps resolve-stream input-name index)
(fn [event]
(let [raw-value (-> event dom/get-target dom/get-input-value)
value (if (tinycolor/hex-without-hash-prefix? raw-value)
(dm/str "#" raw-value)
raw-value)]
(on-composite-indexed-input-token-change form input-name index value composite-type true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:value (or value "")
:hint-message (:message hint)
:slot-start swatch
:hint-type (:type hint)})
props
(if error
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name index composite-type]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
(assoc error :message ((:error/fn error) (:error/value error)))))))
(rx/subs!
(fn [{:keys [error value]}]
(cond
(and error (str/empty? (:error/value error)))
(do
(swap! form update-in [:errors :value composite-type index] dissoc input-name)
(swap! form update-in [:data :value composite-type index] dissoc input-name)
(swap! form assoc-in [:data :value composite-type index :color-result] "")
(swap! form update :extra-errors dissoc :value)
(reset! hint* {}))
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value composite-type index input-name] {:message error'})
(swap! form assoc-in [:data :value composite-type index :color-result] "")
(reset! hint* {:message error' :type "error"}))
:else
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))
input-value (get-in @form [:data :value composite-type index input-name] "")]
(swap! form update :errors dissoc :value)
(swap! form update :extra-errors dissoc :value)
(swap! form assoc-in [:data :value composite-type index :color-result] (dwtf/format-token-value value))
(if (= input-value (str value))
(reset! hint* {})
(reset! hint* {:message message :type "hint"})))))))]
(fn []
(rx/dispose! subs))))
[:*
[:> input* props]
(when color-ramp-open?
[:> ramp* {:color value :on-change on-change-value}])]))

View File

@@ -0,0 +1,298 @@
;; 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.combobox-token-fonts
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.types.token :as cto]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.fonts :as fonts]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- resolve-value
[tokens prev-token value]
(let [token
{:value (cto/split-font-family value)
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
(mf/defc font-picker-combobox*
[{:keys [token tokens name] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
touched?
(and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error
(get-in @form [:errors input-name])
value
(get-in @form [:data input-name] "")
font (fonts/find-font-family value)
resolve-stream
(mf/with-memo [token]
(if-let [value (:value token)]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
font-selector-open* (mf/use-state false)
font-selector-open? (deref font-selector-open*)
on-click-dropdown-button
(mf/use-fn
(mf/deps font-selector-open?)
(fn [e]
(dom/prevent-default e)
(reset! font-selector-open* (not font-selector-open?))))
font-selector-button
(mf/html
[:> icon-button*
{:on-click on-click-dropdown-button
:aria-label (tr "workspace.tokens.token-font-family-select")
:icon i/arrow-down
:variant "action"
:type "button"}])
on-close-font-selector
(mf/use-fn
(fn []
(reset! font-selector-open* false)))
on-select-font
(mf/use-fn
(mf/deps font)
(fn [{:keys [family] :as font}]
(when (not= value family)
(fm/on-input-change form input-name family true)
(rx/push! resolve-stream family))))
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value false)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:value (or value "")
:hint-message (:message hint)
:slot-end font-selector-button
:variant "comfortable"
:hint-type (:type hint)})
props
(if (and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name touched?]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs! (fn [{:keys [error value]}]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"}))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc input-name)
(reset! hint* {:message message :type "hint"})))))))]
(fn []
(rx/dispose! subs))))
[:*
[:> input* props]
(when font-selector-open?
[:div {:class (stl/css :font-select-wrapper)}
[:> font-selector* {:current-font font
:on-select on-select-font
:on-close on-close-font-selector
:full-size true}]])]))
(defn- on-composite-combobox-token-change
([form field value]
(on-composite-combobox-token-change form field value false))
([form field value trim?]
(letfn [(clean-errors [errors]
(-> errors
(dissoc field)
(not-empty)))]
(swap! form (fn [state]
(-> state
(assoc-in [:data :value field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc font-picker-composite-combobox*
[{:keys [token tokens name] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
error
(get-in @form [:errors :value input-name])
value
(get-in @form [:data :value input-name] "")
font (fonts/find-font-family value)
resolve-stream
(mf/with-memo [token]
(if-let [value (get-in token [:value input-name])]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
font-selector-open* (mf/use-state false)
font-selector-open? (deref font-selector-open*)
on-click-dropdown-button
(mf/use-fn
(mf/deps font-selector-open?)
(fn [e]
(dom/prevent-default e)
(reset! font-selector-open* (not font-selector-open?))))
font-selector-button
(mf/html
[:> icon-button*
{:on-click on-click-dropdown-button
:aria-label (tr "workspace.tokens.token-font-family-select")
:icon i/arrow-down
:variant "action"
:type "button"}])
on-close-font-selector
(mf/use-fn
(fn []
(reset! font-selector-open* false)))
on-select-font
(mf/use-fn
(mf/deps font)
(fn [{:keys [family] :as font}]
(when (not= value family)
(on-composite-combobox-token-change form input-name family true)
(rx/push! resolve-stream family))))
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(on-composite-combobox-token-change form input-name value false)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:value (or value "")
:hint-message (:message hint)
:slot-end font-selector-button
:variant "comfortable"
:hint-type (:type hint)})
props
(if error
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs!
(fn [{:keys [error value]}]
(cond
(and error (str/empty? (:error/value error)))
(do
(swap! form update-in [:errors :value] dissoc input-name)
(swap! form update-in [:data :value] dissoc input-name)
(swap! form update :extra-errors dissoc :value)
(reset! hint* {}))
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
:else
(let [message (tr "workspace.tokens.resolved-value" value)
input-value (get-in @form [:data :value input-name] "")]
(swap! form update :errors dissoc :value)
(swap! form update :extra-errors dissoc :value)
(if (or (empty? value) (= input-value value))
(reset! hint* {})
(reset! hint* {:message message :type "hint"})))))))]
(fn []
(rx/dispose! subs))))
[:*
[:> input* props]
(when font-selector-open?
[:div {:class (stl/css :font-select-wrapper)}
[:> font-selector* {:current-font font
:on-select on-select-font
:on-close on-close-font-selector
:full-size true}]])]))

View File

@@ -0,0 +1,14 @@
// 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/_utils.scss" as *;
.font-select-wrapper {
position: absolute;
width: 100%;
height: px2rem(115);
top: px2rem(54);
}

View File

@@ -23,7 +23,7 @@
[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.main.ui.workspace.tokens.management.create.input-token :refer [input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@@ -52,8 +52,6 @@
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
@@ -82,7 +80,7 @@
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [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.
@@ -169,7 +167,9 @@
: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)]
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
@@ -186,7 +186,7 @@
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-input-token*
[:> input-token*
{:placeholder (tr "workspace.tokens.token-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value

View File

@@ -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.font-family
(:require-macros [app.main.style :as stl])
(:require
[app.common.files.tokens :as cft]
[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/token-name-ref 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 (cft/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)
(nil? (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)}
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(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")]]]]))

View File

@@ -0,0 +1,59 @@
// 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/typography.scss" as t;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
.form-wrapper {
width: $sz-384;
position: relative;
}
.token-rows {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.input-row {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
position: relative;
}
.title-bar {
display: grid;
grid-template-columns: 1fr auto;
}
.form-modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.button-row {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
gap: var(--sp-m);
padding-block-start: var(--sp-s);
}
.with-delete {
grid-template-columns: 1fr auto auto;
}
.warning-name-change-notification-wrapper {
margin-block-start: var(--sp-l);
}
.delete-btn {
justify-self: start;
}

View File

@@ -31,16 +31,18 @@
[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.icons :as deprecated-icon]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.main.ui.workspace.sidebar.options.menus.typography :refer [font-selector*]]
[app.main.ui.workspace.tokens.management.create.border-radius :as border-radius]
[app.main.ui.workspace.tokens.management.create.color :as color]
[app.main.ui.workspace.tokens.management.create.dimensions :as dimensions]
[app.main.ui.workspace.tokens.management.create.font-family :as font-family]
[app.main.ui.workspace.tokens.management.create.input-token-color-bullet :refer [input-token-color-bullet*]]
[app.main.ui.workspace.tokens.management.create.input-tokens-value :refer [input-token* token-value-hint*]]
[app.main.ui.workspace.tokens.management.create.shadow :as shadow]
[app.main.ui.workspace.tokens.management.create.text-case :as text-case]
[app.main.ui.workspace.tokens.management.create.typography :as typography]
[app.util.dom :as dom]
[app.util.functions :as uf]
[app.util.i18n :refer [tr]]
@@ -132,8 +134,9 @@
[check-token-empty-value check-self-reference])
(defn- default-validate-token
"Validates a token by confirming a list of `validator` predicates and resolving the token using `tokens` with StyleDictionary.
Returns rx stream of either a valid resolved token or an errors map.
"Validates a token by confirming a list of `validator` predicates and
resolving the token using `tokens` with StyleDictionary. Returns rx
stream of either a valid resolved token or an errors map.
Props:
token-name, token-value, token-description: Values from the form inputs
@@ -208,7 +211,7 @@
(defn- check-empty-shadow-token [token]
(when (or (empty? (:value token))
(some (fn [shadow] (not-every? #(contains? shadow %) [:offsetX :offsetY :blur :spread :color]))
(some (fn [shadow] (not-every? #(contains? shadow %) [:offset-x :offset-y :blur :spread :color]))
(:value token)))
(wte/get-error-code :error.token/empty-input)))
@@ -231,25 +234,26 @@
(default-validate-token))))
(defn- validate-shadow-token
[{:keys [token-value] :as props}]
[{:keys [token-value] :as params}]
(cond
;; Entering form without a value - show no error just resolve nil
(nil? token-value) (rx/of nil)
;; Validate refrence string
(cto/shadow-composite-token-reference? token-value) (default-validate-token props)
(cto/shadow-composite-token-reference? token-value) (default-validate-token params)
;; Validate composite token
:else
(-> props
(update :token-value (fn [value]
(->> (or value [])
(mapv (fn [shadow]
(d/update-when shadow :inset #(cond
(boolean? %) %
(= "true" %) true
:else false)))))))
(assoc :validators [check-empty-shadow-token
check-shadow-token-self-reference])
(default-validate-token))))
(let [params (-> params
(update :token-value (fn [value]
(->> (or value [])
(mapv (fn [shadow]
(d/update-when shadow :inset #(cond
(boolean? %) %
(= "true" %) true
:else false)))))))
(assoc :validators [check-empty-shadow-token
check-shadow-token-self-reference]))]
(default-validate-token params))))
(defn- use-debonced-resolve-callback
"Resolves a token values using `StyleDictionary`.
@@ -735,11 +739,11 @@
:selected (if reference-tab-active? "reference" "composite")
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon deprecated-icon/layers
[:& radio-button {:icon i/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon deprecated-icon/tokens
[:& radio-button {:icon i/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]]
@@ -936,10 +940,10 @@
(def ^:private shadow-inputs
#(d/ordered-map
:offsetX
:offset-x
{:label (tr "workspace.tokens.shadow-x")
:placeholder (tr "workspace.tokens.shadow-x")}
:offsetY
:offset-y
{:label (tr "workspace.tokens.shadow-y")
:placeholder (tr "workspace.tokens.shadow-y")}
:blur
@@ -974,11 +978,11 @@
:name (str "inset-select-" shadow-idx)}
[:& radio-button {:value "false"
:title "false"
:icon "❌"
:icon i/close
:id (str "inset-default-" shadow-idx)}]
[:& radio-button {:value "true"
:title "true"
:icon "✅"
:icon i/tick
:id (str "inset-false-" shadow-idx)}]]]))
(mf/defc shadow-input*
@@ -1437,13 +1441,16 @@
(mf/spread-props props {:token-type token-type
:validate-token default-validate-token
:tokens-tree-in-selected-set tokens-tree-in-selected-set
:token token})]
:token token})
font-family-props (mf/spread-props props {:validate-token validate-font-family-token})
typography-props (mf/spread-props props {:validate-token validate-typography-token})
shadow-props (mf/spread-props props {:validate-token validate-shadow-token})]
(case token-type
:color [:> color/form* props]
:typography [:> typography-form* props]
:shadow [:> shadow-form* props]
:font-family [:> font-family-form* props]
:typography [:> typography/form* typography-props]
:shadow [:> shadow/form* shadow-props]
:font-family [:> font-family/form* font-family-props]
:text-case [:> text-case/form* props]
:text-decoration [:> text-decoration-form* props]
:font-weight [:> font-weight-form* props]

View File

@@ -1,245 +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.workspace.tokens.management.create.form-color-input-token
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.types.color :as cl]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.tinycolor :as tinycolor]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.utilities.swatch :refer [swatch*]]
[app.main.ui.forms :as fc]
[app.main.ui.workspace.colorpicker :as colorpicker]
[app.main.ui.workspace.colorpicker.ramp :refer [ramp-selector*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[clojure.core :as c]
[rumext.v2 :as mf]))
(defn- resolve-value
[tokens prev-token value]
(let [token
{:value value
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
(defn- hex->color-obj
[hex]
(when-let [tc (tinycolor/valid-color hex)]
(let [hex (tinycolor/->hex-string tc)
alpha (tinycolor/alpha tc)
[r g b] (cl/hex->rgb hex)
[h s v] (cl/hex->hsv hex)]
{:hex hex
:r r :g g :b b
:h h :s s :v v
:alpha alpha})))
(mf/defc ramp*
[{:keys [color on-change]}]
(let [wrapper-node-ref (mf/use-ref nil)
dragging-ref (mf/use-ref false)
on-start-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref true))
on-finish-drag
(mf/use-fn #(mf/set-ref-val! dragging-ref false))
internal-color*
(mf/use-state #(hex->color-obj color))
internal-color
(deref internal-color*)
on-change'
(mf/use-fn
(mf/deps on-change)
(fn [{:keys [hex alpha] :as selector-color}]
(let [dragging? (mf/ref-val dragging-ref)]
(when-not (and dragging? hex)
(reset! internal-color* selector-color)
(on-change hex alpha)))))]
(mf/use-effect
(mf/deps color)
(fn []
;; Update internal color when user changes input value
(when-let [color (tinycolor/valid-color color)]
(when-not (= (tinycolor/->hex-string color) (:hex internal-color))
(reset! internal-color* (hex->color-obj color))))))
(colorpicker/use-color-picker-css-variables! wrapper-node-ref internal-color)
[:div {:ref wrapper-node-ref}
[:> ramp-selector*
{:color internal-color
:on-start-drag on-start-drag
:on-finish-drag on-finish-drag
:on-change on-change'}]]))
(mf/defc form-color-input-token*
[{:keys [name tokens token] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
resolved-input-name
(mf/with-memo [input-name]
(keyword (str "resolved-" (c/name input-name))))
touched?
(and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error
(get-in @form [:errors input-name])
value
(get-in @form [:data input-name] "")
resolved-value
(get-in @form [:data resolved-input-name] "")
hex (if (tinycolor/valid-color resolved-value)
(tinycolor/->hex-string (tinycolor/valid-color resolved-value))
"#8f9da3")
alpha (if (tinycolor/valid-color resolved-value)
(tinycolor/alpha (tinycolor/valid-color resolved-value))
1)
resolve-stream
(mf/with-memo [token]
(if-let [value (:value token)]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
color-ramp-open* (mf/use-state false)
color-ramp-open? (deref color-ramp-open*)
on-click-swatch
(mf/use-fn
(mf/deps color-ramp-open?)
(fn []
(let [open? (not color-ramp-open?)]
(reset! color-ramp-open* open?))))
swatch
(mf/html
[:> swatch*
{:background {:color hex :opacity alpha}
:show-tooltip false
:data-testid "token-form-color-bullet"
:class (stl/css :slot-start)
:on-click on-click-swatch}])
on-change-value
(mf/use-fn
(mf/deps resolve-stream input-name value)
(fn [hex alpha]
(let [;; StyleDictionary will always convert to hex/rgba, so we take the format from the value input field
prev-input-color (some-> value
(tinycolor/valid-color))
;; If the input is a reference we will take the format from the computed value
prev-computed-color (when-not prev-input-color
(some-> value (tinycolor/valid-color)))
prev-format (some-> (or prev-input-color prev-computed-color)
(tinycolor/color-format))
to-rgba? (and
(< alpha 1)
(or (= prev-format "hex") (not prev-format)))
to-hex? (and (not prev-format) (= alpha 1))
format (cond
to-rgba? "rgba"
to-hex? "hex"
prev-format prev-format
:else "hex")
color-value (-> (tinycolor/valid-color hex)
(tinycolor/set-alpha (or alpha 1))
(tinycolor/->string format))]
(when (not= value color-value)
(fm/on-input-change form input-name color-value true)
(rx/push! resolve-stream color-value)))))
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [raw-value (-> event dom/get-target dom/get-input-value)
value (if (tinycolor/hex-without-hash-prefix? raw-value)
(dm/str "#" raw-value)
raw-value)]
(fm/on-input-change form input-name value true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
;; TODO: Review this value vs default-value
:value (or value "")
:hint-message (:message hint)
:variant "comfortable"
:slot-start swatch
:hint-type (:type hint)})
props
(if (and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs! (fn [{:keys [error value]}]
(if error
(do
(swap! form assoc-in [:errors input-name] {:message error})
(swap! form assoc-in [:errors resolved-input-name] {:message error})
(swap! form update :data dissoc resolved-input-name)
(reset! hint* {:message error :type "error"}))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :errors dissoc input-name resolved-input-name)
(swap! form update :data assoc resolved-input-name value)
(reset! hint* {:message message :type "hint"}))))))]
(fn []
(rx/dispose! subs))))
[:*
[:> input* props]
(when color-ramp-open?
[:> ramp* {:color value :on-change on-change-value}])]))

View File

@@ -1,118 +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.workspace.tokens.management.create.form-input-token
(:require
[app.common.data :as d]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.forms :as fc]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[clojure.core :as c]
[rumext.v2 :as mf]))
(defn- resolve-value
[tokens prev-token value]
(let [token
{:value value
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
(mf/defc form-input-token*
[{:keys [name tokens token] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
resolved-input-name
(mf/with-memo [input-name]
(keyword (str "resolved-" (c/name input-name))))
touched?
(and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error
(get-in @form [:errors input-name])
value
(get-in @form [:data input-name] "")
resolve-stream
(mf/with-memo [token]
(if-let [value (:value token)]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:default-value value
:hint-message (:message hint)
:variant "comfortable"
:hint-type (:type hint)})
props
(if (and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs! (fn [{:keys [error value]}]
(if error
(do
(swap! form assoc-in [:errors input-name] {:message error})
(swap! form assoc-in [:errors resolved-input-name] {:message error})
(swap! form update :data dissoc resolved-input-name)
(reset! hint* {:message error :type "error"}))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :errors dissoc input-name resolved-input-name)
(swap! form update :data assoc resolved-input-name value)
(reset! hint* {:message message :type "hint"}))))))]
(fn []
(rx/dispose! subs))))
[:> input* props]))

View File

@@ -0,0 +1,314 @@
;; 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.input-token
(:require
[app.common.data :as d]
[app.common.types.tokens-lib :as ctob]
[app.main.data.style-dictionary :as sd]
[app.main.data.workspace.tokens.format :as dwtf]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.forms :as fc]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
[beicon.v2.core :as rx]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
(defn- resolve-value
[tokens prev-token value]
(let [token
{:value value
:name "__PENPOT__TOKEN__NAME__PLACEHOLDER__"}
tokens
(-> tokens
;; Remove previous token when renaming a token
(dissoc (:name prev-token))
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
(->> tokens
(sd/resolve-tokens-interactive)
(rx/mapcat
(fn [resolved-tokens]
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
(if resolved-value
(rx/of {:value resolved-value})
(rx/of {:error (first errors)}))))))))
(mf/defc input-token*
[{:keys [name tokens token] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
touched?
(and (contains? (:data @form) input-name)
(get-in @form [:touched input-name]))
error
(get-in @form [:errors input-name])
value
(get-in @form [:data input-name] "")
resolve-stream
(mf/with-memo [token]
(if (contains? token :value)
(rx/behavior-subject (:value token))
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(fm/on-input-change form input-name value true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:default-value value
:variant "comfortable"
:hint-message (:message hint)
:hint-type (:type hint)})
props
(if (and error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
((:error/fn error) (:error/value error))))))
(rx/subs! (fn [{:keys [error value]}]
(let [touched? (get-in @form [:touched input-name])]
(when touched?
(if error
(do
(swap! form assoc-in [:extra-errors input-name] {:message error})
(reset! hint* {:message error :type "error"}))
(let [message (tr "workspace.tokens.resolved-value" value)]
(swap! form update :extra-errors dissoc input-name)
(reset! hint* {:message message :type "hint"}))))))))]
(fn []
(rx/dispose! subs))))
[:> input* props]))
(defn- on-composite-input-token-change
([form field value]
(on-composite-input-token-change form field value false))
([form field value trim?]
(letfn [(clean-errors [errors]
(-> errors
(dissoc field)
(not-empty)))]
(swap! form (fn [state]
(-> state
(assoc-in [:data :value field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc input-token-composite*
[{:keys [name tokens token] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
error
(get-in @form [:errors :value input-name])
value
(get-in @form [:data :value input-name] "")
resolve-stream
(mf/with-memo [token]
(if-let [value (get-in token [:value input-name])]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
on-change
(mf/use-fn
(mf/deps resolve-stream input-name)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(on-composite-input-token-change form input-name value true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:default-value value
:variant "comfortable"
:hint-message (:message hint)
:hint-type (:type hint)})
props
(if error
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)
props (if (and (not error) (= input-name :reference))
(mf/spread-props props {:hint-formated true})
props)]
(mf/with-effect [resolve-stream tokens token input-name]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
(assoc error :message ((:error/fn error) (:error/value error)))))))
(rx/subs!
(fn [{:keys [error value]}]
(cond
(and error (str/empty? (:error/value error)))
(do
(swap! form update-in [:errors :value] dissoc input-name)
(swap! form update-in [:data :value] dissoc input-name)
(swap! form update :extra-errors dissoc :value)
(reset! hint* {}))
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
:else
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))
input-value (get-in @form [:data :value input-name] "")]
(swap! form update :errors dissoc :value)
(swap! form update :extra-errors dissoc :value)
(if (= input-value (str value))
(reset! hint* {})
(reset! hint* {:message message :type "hint"})))))
(fn [cause]
(js/console.log "MUU" cause))))]
(fn []
(rx/dispose! subs))))
[:> input* props]))
(defn- on-composite-indexed-input-token-change
([form field index value composite-type]
(on-composite-indexed-input-token-change form field index value composite-type false))
([form field index value composite-type trim?]
(letfn [(clean-errors [errors]
(-> errors
(dissoc field)
(not-empty)))]
(swap! form (fn [state]
(-> state
(assoc-in [:data :value composite-type index field] (if trim? (str/trim value) value))
(update :errors clean-errors)
(update :extra-errors clean-errors)))))))
(mf/defc input-token-indexed*
[{:keys [name tokens token index composite-type] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
error
(get-in @form [:errors :value composite-type index input-name])
value-from-form
(get-in @form [:data :value composite-type index input-name] "")
resolve-stream
(mf/with-memo [token index input-name]
(if-let [value (get-in token [:value composite-type index input-name])]
(rx/behavior-subject value)
(rx/subject)))
hint*
(mf/use-state {})
hint
(deref hint*)
on-change
(mf/use-fn
(mf/deps resolve-stream input-name index)
(fn [event]
(let [value (-> event dom/get-target dom/get-input-value)]
(on-composite-indexed-input-token-change form input-name index value composite-type true)
(rx/push! resolve-stream value))))
props
(mf/spread-props props {:on-change on-change
:value value-from-form
:variant "comfortable"
:hint-message (:message hint)
:hint-type (:type hint)})
props
(if error
(mf/spread-props props {:hint-type "error"
:hint-message (:message error)})
props)
props
(if (and (not error) (= input-name :reference))
(mf/spread-props props {:hint-formated true})
props)]
(mf/with-effect [resolve-stream tokens token input-name index composite-type]
(let [subs (->> resolve-stream
(rx/debounce 300)
(rx/mapcat (partial resolve-value tokens token))
(rx/map (fn [result]
(d/update-when result :error
(fn [error]
(assoc error :message ((:error/fn error) (:error/value error)))))))
(rx/subs!
(fn [{:keys [error value]}]
(cond
(and error (str/empty? (:error/value error)))
(do
(swap! form update-in [:errors :value composite-type index] dissoc input-name)
(swap! form update-in [:data :value composite-type index] dissoc input-name)
(swap! form update :extra-errors dissoc :value)
(reset! hint* {}))
(some? error)
(let [error' (:message error)]
(swap! form assoc-in [:extra-errors :value composite-type index input-name] {:message error'})
(reset! hint* {:message error' :type "error"}))
:else
(let [message (tr "workspace.tokens.resolved-value" (dwtf/format-token-value value))
input-value (get-in @form [:data :value composite-type index input-name] "")]
(swap! form update :errors dissoc :value)
(swap! form update :extra-errors dissoc :value)
(if (= input-value (str value))
(reset! hint* {})
(reset! hint* {:message message :type "hint"})))))))]
(fn []
(rx/dispose! subs))))
[:> input* props]))

View File

@@ -0,0 +1,31 @@
;; 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.select-token
(:require
[app.main.ui.ds.controls.select :refer [select*]]
[app.main.ui.forms :as fc]
[rumext.v2 :as mf]))
(mf/defc select-composite*
[{:keys [name index composite-type] :rest props}]
(let [form (mf/use-ctx fc/context)
input-name name
value
(get-in @form [:data :value composite-type index input-name] false)
on-change
(mf/use-fn
(mf/deps input-name)
(fn [type]
(let [is-inner? (= type "inner")]
(swap! form assoc-in [:data :value composite-type index input-name] is-inner?))))
props (mf/spread-props props {:default-selected (if value "inner" "drop")
:variant "ghost"
:on-change on-change})]
[:> select* props]))

View File

@@ -0,0 +1,486 @@
;; 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.shadow
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[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.components.radio-buttons :refer [radio-button radio-buttons]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.forms :as forms]
[app.main.ui.hooks :as hooks]
[app.main.ui.workspace.tokens.management.create.color-input-token :refer [color-input-token-indexed*]]
[app.main.ui.workspace.tokens.management.create.input-token :refer [input-token-composite* input-token-indexed*]]
[app.main.ui.workspace.tokens.management.create.select-token :refer [select-composite*]]
[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]))
(def ^:private default-token-shadow
{:offset-x "4"
:offset-y "4"
:blur "4"
:spread "0"})
(defn get-subtoken
[token index prop composite-type]
(let [value (get-in token [:value composite-type index prop])]
(d/without-nils
{:type (if (= prop :color) :color :number)
:value value})))
(mf/defc shadow-formset*
[{:keys [index token tokens remove-shadow-block show-button composite-type] :as props}]
(let [inset-token (get-subtoken token index :inset composite-type)
inset-token (hooks/use-equal-memo inset-token)
color-token (get-subtoken token index :color composite-type)
color-token (hooks/use-equal-memo color-token)
offset-x-token (get-subtoken token index :offset-x composite-type)
offset-x-token (hooks/use-equal-memo offset-x-token)
offset-y-token (get-subtoken token index :offset-y composite-type)
offset-y-token (hooks/use-equal-memo offset-y-token)
blur-token (get-subtoken token index :blur composite-type)
blur-token (hooks/use-equal-memo blur-token)
spread-token (get-subtoken token index :spreadX composite-type)
spread-token (hooks/use-equal-memo spread-token)
on-button-click
(mf/use-fn
(mf/deps index)
(fn [event]
(remove-shadow-block index event)))]
[:div {:class (stl/css :shadow-block)
:data-testid (str "shadow-input-fields-" index)}
[:div {:class (stl/css :select-wrapper)}
[:> select-composite* {:options [{:id "drop" :label "drop shadow" :icon i/drop-shadow}
{:id "inner" :label "inner shadow" :icon i/inner-shadow}]
:aria-label (tr "workspace.tokens.shadow-inset")
:token inset-token
:tokens tokens
:index index
:composite-type composite-type
:name :inset}]
(when show-button
[:> icon-button* {:variant "ghost"
:type "button"
:aria-label (tr "workspace.tokens.shadow-remove-shadow")
:on-click on-button-click
:icon i/remove}])]
[:div {:class (stl/css :inputs-wrapper)}
[:div {:class (stl/css :input-row)}
[:> color-input-token-indexed*
{:placeholder (tr "workspace.tokens.token-value-enter")
:aria-label (tr "workspace.tokens.color")
:name :color
:token color-token
:composite-type composite-type
:index index
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-indexed*
{:aria-label (tr "workspace.tokens.shadow-x")
:icon i/character-x
:placeholder (tr "workspace.tokens.shadow-x")
:name :offset-x
:token offset-x-token
:index index
:composite-type composite-type
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-indexed*
{:aria-label (tr "workspace.tokens.shadow-y")
:icon i/character-y
:placeholder (tr "workspace.tokens.shadow-y")
:name :offset-y
:token offset-y-token
:index index
:composite-type composite-type
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-indexed*
{:aria-label (tr "workspace.tokens.shadow-blur")
:placeholder (tr "workspace.tokens.shadow-blur")
:name :blur
:slot-start (mf/html [:span {:class (stl/css :visible-label)}
(str (tr "workspace.tokens.shadow-blur") ":")])
:token blur-token
:index index
:composite-type composite-type
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-indexed*
{:aria-label (tr "workspace.tokens.shadow-spread")
:placeholder (tr "workspace.tokens.shadow-spread")
:name :spread
:slot-start (mf/html [:span {:class (stl/css :visible-label)}
(str (tr "workspace.tokens.shadow-spread") ":")])
:token spread-token
:composite-type composite-type
:index index
:tokens tokens}]]]]))
(mf/defc composite-form*
[{:keys [token tokens remove-shadow-block composite-type] :as props}]
(let [form
(mf/use-ctx forms/context)
length
(-> form deref :data :value composite-type count)]
(for [index (range length)]
[:> shadow-formset* {:key index
:index index
:token token
:tokens tokens
:composite-type composite-type
:remove-shadow-block remove-shadow-block
:show-button (> length 1)}])))
(mf/defc reference-form*
[{:keys [token tokens] :as props}]
[:div {:class (stl/css :input-row-reference)}
[:> input-token-composite*
{:placeholder (tr "workspace.tokens.reference-composite-shadow")
:aria-label (tr "labels.reference")
:icon i/drop-shadow
:name :reference
:token token
:tokens tokens}]])
(defn- make-schema
[tokens-tree active-tab]
(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/token-name-ref 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 (cft/token-name-path-exists? % tokens-tree))]]]
[:value
[:map
[:shadow {:optinal true}
[:vector
[:map
[:offset-x {:optional true} [:maybe :string]]
[:offset-y {:optional true} [:maybe :string]]
[:blur {:optional true} [:maybe :string]]
[:spread {:optional true} [:maybe :string]]
[:color {:optional true} [:maybe :string]]
[:color-result {:optional true} ::sm/any]
[:inset {:optional true} [:maybe :boolean]]]]]
(if (= active-tab :reference)
[:reference {:optional false} ::sm/text]
[:reference {:optional true} [:maybe :string]])]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field [:value :reference]
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(let [reference (get value :reference)]
(if (and reference name)
(not (cto/token-value-self-reference? name reference))
true)))]
[:fn {:error/fn (fn [_] "Must be a valid shadow or reference")
:error/field :value}
(fn [{:keys [value]}]
(let [reference (get value :reference)
ref-valid? (and reference (not (str/blank? reference)))
shadows (get value :shadow)
;; To be a valid shadow it must contain one on each valid values
valid-composite-shadow?
(and (seq shadows)
(every?
(fn [{:keys [offset-x offset-y blur spread color]}]
(and (not (str/blank? offset-x))
(not (str/blank? offset-y))
(not (str/blank? blur))
(not (str/blank? spread))
(not (str/blank? color))))
shadows))]
(or ref-valid? valid-composite-shadow?)))]]))
(mf/defc form*
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
(let [active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
composite-type :shadow
token
(mf/with-memo [token]
(or token
(if-let [value (get token :value)]
(cond
(string? value)
{:value {:reference value
:shadow []}
:type :shadow}
(vector? value)
{:value {:reference nil
:shadow value}
:type :shadow})
{:type :shadow
:value {:reference nil
:shadow [default-token-shadow]}})))
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 active-tab]
(make-schema tokens-tree-in-selected-set active-tab))
initial
(mf/with-memo [token]
(let [raw-value (:value token)
value
(cond
(string? raw-value)
{:reference raw-value
:shadow []}
(vector? raw-value)
{:reference nil
:shadow raw-value}
:else
{:reference nil
:shadow [default-token-shadow]})]
{:name (:name token "")
:description (:description token "")
:value value}))
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-toggle-tab
(mf/use-fn
(mf/deps)
(fn [new-tab]
(let [new-tab (keyword new-tab)]
(reset! active-tab* new-tab))))
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-add-shadow-block
(mf/use-fn
(mf/deps composite-type)
(fn []
(swap! form update-in [:data :value composite-type] conj default-token-shadow)))
remove-shadow-block
(mf/use-fn
(mf/deps composite-type)
(fn [index event]
(dom/prevent-default event)
(swap! form update-in [:data :value composite-type] #(d/remove-at-index % index))))
on-submit
(mf/use-fn
(mf/deps validate-token token tokens token-type active-tab composite-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 (if (= active-tab :reference)
(:reference value)
(composite-type 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))))))))]
[:> forms/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)}
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> forms/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 :title-bar)}
[:div {:class (stl/css :title)} (tr "labels.shadow")]
[:> icon-button* {:variant "ghost"
:type "button"
:aria-label (tr "workspace.tokens.shadow-add-shadow")
:on-click on-add-shadow-block
:icon i/add}]
[:& radio-buttons {:class (stl/css :listing-options)
:selected (d/name active-tab)
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon i/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon i/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]]
(if (= active-tab :composite)
[:> composite-form* {:token token
:tokens tokens
:remove-shadow-block remove-shadow-block
:composite-type composite-type}]
[:> reference-form* {:token token
:tokens tokens}])
[:div {:class (stl/css :input-row)}
[:> forms/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")]
[:> forms/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -0,0 +1,95 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/typography.scss" as t;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
.form-wrapper {
width: $sz-384;
position: relative;
}
.token-rows {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.inputs-wrapper {
display: flex;
flex-direction: column;
gap: var(--sp-m);
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
padding-inline-start: var(--sp-m);
}
.input-row {
position: relative;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.input-row-reference {
position: relative;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
padding-inline-start: var(--sp-m);
}
.title-bar {
display: grid;
grid-template-columns: 1fr auto auto;
gap: var(--sp-xs);
}
.title {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.form-modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.button-row {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
gap: var(--sp-m);
padding-block-start: var(--sp-s);
}
.with-delete {
grid-template-columns: 1fr auto auto;
}
.warning-name-change-notification-wrapper {
margin-block-start: var(--sp-l);
}
.delete-btn {
justify-self: start;
}
.visible-label {
@include t.use-typography("headline-small");
color: var(--color-foreground-secondary);
line-height: $sz-32;
}
.select-wrapper {
display: grid;
grid-template-columns: 1fr auto;
}

View File

@@ -23,7 +23,7 @@
[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.main.ui.workspace.tokens.management.create.input-token :refer [input-token*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :refer [tr]]
@@ -52,8 +52,6 @@
[:value [::sm/text {:error/fn token-value-error-fn}]]
[:resolved-value ::sm/any]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
@@ -82,7 +80,7 @@
(mf/deref refs/workspace-active-theme-sets-tokens)
tokens
(mf/with-memo [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.
@@ -170,7 +168,9 @@
[: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)]
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> fc/form-input* {:id "token-name"
@@ -187,7 +187,7 @@
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
[:div {:class (stl/css :input-row)}
[:> form-input-token*
[:> input-token*
{:placeholder (tr "workspace.tokens.text-case-value-enter")
:label (tr "workspace.tokens.token-value")
:name :value

View File

@@ -0,0 +1,429 @@
;; 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.typography
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.files.tokens :as cft]
[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.components.radio-buttons :refer [radio-button radio-buttons]]
[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 forms]
[app.main.ui.workspace.tokens.management.create.combobox-token-fonts :refer [font-picker-composite-combobox*]]
[app.main.ui.workspace.tokens.management.create.input-token :refer [input-token-composite*]]
[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]))
(mf/defc composite-form*
[{:keys [token tokens] :as props}]
(let [letter-spacing-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :letter-spacing
:value (cto/join-font-family (get value :letter-spacing))}
{:type :letter-spacing}))
font-family-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-family
:value (get value :font-family)}
{:type :font-family}))
font-size-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-size
:value (get value :font-size)}
{:type :font-size}))
font-weight-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :font-weight
:value (get value :font-weight)}
{:type :font-weight}))
;; TODO: Review this type
line-height-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :number
:value (get value :line-height)}
{:type :number}))
text-case-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :text-case
:value (get value :text-case)}
{:type :text-case}))
text-decoration-sub-token
(mf/with-memo [token]
(if-let [value (get token :value)]
{:type :text-decoration
:value (get value :text-decoration)}
{:type :text-decoration}))]
[:*
[:div {:class (stl/css :input-row)}
[:> font-picker-composite-combobox*
{:icon i/text-font-family
:placeholder (tr "workspace.tokens.token-font-family-value-enter")
:aria-label (tr "workspace.tokens.token-font-family-value")
:name :font-family
:token font-family-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Font Size"
:icon i/text-font-size
:placeholder (tr "workspace.tokens.font-size-value-enter")
:name :font-size
:token font-size-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Font Weight"
:icon i/text-font-weight
:placeholder (tr "workspace.tokens.font-weight-value-enter")
:name :font-weight
:token font-weight-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Line Height"
:icon i/text-lineheight
:placeholder (tr "workspace.tokens.line-height-value-enter")
:name :line-height
:token line-height-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Letter Spacing"
:icon i/text-letterspacing
:placeholder (tr "workspace.tokens.letter-spacing-value-enter-composite")
:name :letter-spacing
:token letter-spacing-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Text Case"
:icon i/text-mixed
:placeholder (tr "workspace.tokens.text-case-value-enter")
:name :text-case
:token text-case-sub-token
:tokens tokens}]]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:aria-label "Text Decoration"
:icon i/text-underlined
:placeholder (tr "workspace.tokens.text-decoration-value-enter")
:name :text-decoration
:token text-decoration-sub-token
:tokens tokens}]]]))
(mf/defc reference-form*
[{:keys [token tokens] :as props}]
[:div {:class (stl/css :input-row)}
[:> input-token-composite*
{:placeholder (tr "workspace.tokens.reference-composite")
:aria-label (tr "labels.reference")
:icon i/text-typography
:name :reference
:token token
:tokens tokens}]])
(defn- make-schema
[tokens-tree active-tab]
(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/token-name-ref 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 (cft/token-name-path-exists? % tokens-tree))]]]
[:value
[:map
[:font-family {:optional true} [:maybe :string]]
[:font-size {:optional true} [:maybe :string]]
[:font-weight {:optional true} [:maybe :string]]
[:line-height {:optional true} [:maybe :string]]
[:letter-spacing {:optional true} [:maybe :string]]
[:text-case {:optional true} [:maybe :string]]
[:text-decoration {:optional true} [:maybe :string]]
(if (= active-tab :reference)
[:reference {:optional false} ::sm/text]
[:reference {:optional true} [:maybe :string]])]]
[:description {:optional true}
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
[:fn {:error/field [:value :reference]
:error/fn #(tr "workspace.tokens.self-reference")}
(fn [{:keys [name value]}]
(let [reference (get value :reference)]
(if (and reference name)
(not (cto/token-value-self-reference? name reference))
true)))]
[:fn {:error/field [:value :line-height]
:error/fn #(tr "workspace.tokens.composite-line-height-needs-font-size")}
(fn [{:keys [value]}]
(let [line-heigh (get value :line-height)
font-size (get value :font-size)]
(if (and line-heigh (not font-size))
false
true)))]
;; This error does not shown on interface, it's just to avoid saving empty composite tokens
;; We don't need to translate it.
[:fn {:error/fn (fn [_] "At least one composite field must be set")
:error/field :value}
(fn [attrs]
(let [result (reduce-kv (fn [_ _ v]
(if (str/empty? v)
false
(reduced true)))
false
(get attrs :value))]
result))]]))
(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 :typography}))
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*)
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 active-tab]
(make-schema tokens-tree-in-selected-set active-tab))
initial
(mf/with-memo [token]
(let [value (:value token)
processed-value
(cond
(string? value)
{:reference value}
(map? value)
(let [value (cond-> value
(:font-family value)
(update :font-family cto/join-font-family))]
(select-keys value
[:font-family
:font-size
:font-weight
:line-height
:letter-spacing
:text-case
:text-decoration]))
:else
{})]
{:name (:name token "")
:value processed-value
:description (:description token "")}))
form
(fm/use-form :schema schema
:initial initial)
warning-name-change?
(not= (get-in @form [:data :name])
(:name initial))
on-toggle-tab
(mf/use-fn
(mf/deps)
(fn [new-tab]
(let [new-tab (keyword new-tab)]
(reset! active-tab* new-tab))))
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 (if (contains? value :reference)
(get value :reference)
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))))))))]
[:> forms/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)}
(if (= action "edit")
(tr "workspace.tokens.edit-token" token-type)
(tr "workspace.tokens.create-token" token-type))]
[:div {:class (stl/css :input-row)}
[:> forms/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 :title-bar)}
[:div {:class (stl/css :title)} (tr "labels.typography")]
[:& radio-buttons {:class (stl/css :listing-options)
:selected (d/name active-tab)
:on-change on-toggle-tab
:name "reference-composite-tab"}
[:& radio-button {:icon i/layers
:value "composite"
:title (tr "workspace.tokens.individual-tokens")
:id "composite-opt"}]
[:& radio-button {:icon i/tokens
:value "reference"
:title (tr "workspace.tokens.use-reference")
:id "reference-opt"}]]]
[:div {:class (stl/css :inputs-wrapper)}
(if (= active-tab :composite)
[:> composite-form* {:token token
:tokens tokens}]
[:> reference-form* {:token token
:tokens tokens}])]
[:div {:class (stl/css :input-row)}
[:> forms/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")]
[:> forms/form-submit* {:variant "primary"
:on-submit on-submit}
(tr "labels.save")]]]]))

View File

@@ -0,0 +1,74 @@
// 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/typography.scss" as t;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
.form-wrapper {
width: $sz-384;
position: relative;
}
.token-rows {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.inputs-wrapper {
display: flex;
flex-direction: column;
gap: var(--sp-m);
border-inline-start: $b-1 solid var(--color-accent-primary-muted);
padding-inline-start: var(--sp-m);
}
.input-row {
position: relative;
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.title-bar {
display: grid;
grid-template-columns: 1fr auto;
}
.title {
@include t.use-typography("body-small");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.form-modal-title {
@include t.use-typography("headline-medium");
color: var(--color-foreground-primary);
display: flex;
align-items: center;
}
.button-row {
display: grid;
grid-template-columns: auto auto;
justify-content: end;
gap: var(--sp-m);
padding-block-start: var(--sp-s);
}
.with-delete {
grid-template-columns: 1fr auto auto;
}
.warning-name-change-notification-wrapper {
margin-block-start: var(--sp-l);
}
.delete-btn {
justify-self: start;
}

View File

@@ -43,6 +43,7 @@
:stroke-width "stroke-size"
:dimensions "expand"
:sizing "expand"
:shadow "drop-shadow"
"add"))
(mf/defc token-group*

View File

@@ -25,7 +25,6 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.workspace.tokens.sets.lists :as wts]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
@@ -94,11 +93,11 @@
:name name}
[:& radio-button {:id :on
:value :on
:icon deprecated-icon/tick
:icon i/tick
:label ""}]
[:& radio-button {:id :off
:value :off
:icon deprecated-icon/close
:icon i/close
:label ""}]]))
(mf/defc themes-overview

View File

@@ -100,8 +100,8 @@
[{:keys [x y stroke action-type arrow-dir zoom] :as props}]
(let [icon-pdata (case action-type
:navigate (case arrow-dir
:right "M -6.5 0 l 12 0 l -6 -6 m 6 6 l -6 6"
:left "M 6.5 0 l -12 0 l 6 -6 m -6 6 l 6 6"
:right "M -6.5 0 L 5.5 0 M 6.715 0.715 L -0.5 -6.5 M 6.715 -0.715 L -0.365 6.635"
:left "M 6.5 0 l -12 0 m -0.715 0.715 l 6.5 -6.9 m -6 6 l 6 6.35"
nil)
:open-overlay "M-5 -5 h7 v7 h-7 z M2 -2 h3.5 v7 h-7 v-2.5"

View File

@@ -17,7 +17,6 @@
[app.common.uuid :as uuid]
[app.main.data.common :as dcm]
[app.main.data.workspace :as dw]
[app.main.data.workspace.interactions :as dwi]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.streams :as ms]
@@ -277,7 +276,6 @@
pos (gpt/point x (- y (/ 35 zoom)))
frame-id (:id frame)
flow-id (:id flow)
flow-name (:name flow)
on-pointer-down
@@ -291,11 +289,6 @@
(dom/stop-propagation event)
(st/emit! (dcm/go-to-viewer params))))))
on-double-click
(mf/use-fn
(mf/deps flow-id)
#(st/emit! (dwi/start-rename-flow flow-id)))
on-pointer-enter
(mf/use-fn
(mf/deps frame-id on-frame-enter)
@@ -319,7 +312,6 @@
[:div {:class (stl/css-case :frame-flow-badge-content true
:selected is-selected)
:on-pointer-down on-pointer-down
:on-double-click on-double-click
:on-pointer-enter on-pointer-enter
:on-pointer-leave on-pointer-leave}
[:> icon* {:icon-id i/play

View File

@@ -7,7 +7,7 @@
(ns app.plugins.comments
(:require
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.main.data.comments :as dc]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.comments :as dwc]
@@ -118,7 +118,8 @@
(fn [position]
(let [position (parser/parse-point position)]
(cond
(or (not (us/safe-number? (:x position))) (not (us/safe-number? (:y position))))
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :position "Not valid point")
(not (r/check-permission plugin-id "comment:write"))

View File

@@ -7,7 +7,7 @@
(ns app.plugins.flex
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.transforms :as dwt]
@@ -133,7 +133,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -148,7 +148,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -163,7 +163,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -178,7 +178,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -194,7 +194,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -209,7 +209,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -224,7 +224,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -239,7 +239,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -296,7 +296,7 @@
:set
(fn [_ value]
(cond
(us/safe-int? value)
(sm/valid-safe-int? value)
(u/display-not-valid :zIndex value)
(not (r/check-permission plugin-id "content:write"))
@@ -359,7 +359,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :verticalMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -374,7 +374,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :horizontalMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -389,7 +389,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :topMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -404,7 +404,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :rightMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -419,7 +419,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :bottomMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -434,7 +434,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :leftMargin value)
(not (r/check-permission plugin-id "content:write"))
@@ -449,7 +449,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxWidth value)
(not (r/check-permission plugin-id "content:write"))
@@ -464,7 +464,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :minWidth value)
(not (r/check-permission plugin-id "content:write"))
@@ -479,7 +479,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :maxHeight value)
(not (r/check-permission plugin-id "content:write"))
@@ -494,7 +494,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :minHeight value)
(not (r/check-permission plugin-id "content:write"))

View File

@@ -151,8 +151,8 @@
;; export interface Shadow {
;; id?: string;
;; style?: 'drop-shadow' | 'inner-shadow';
;; offsetX?: number;
;; offsetY?: number;
;; offset-x?: number;
;; offset-y?: number;
;; blur?: number;
;; spread?: number;
;; hidden?: boolean;
@@ -164,8 +164,8 @@
(obj/without-empty
#js {:id (-> id format-id)
:style (-> style format-key)
:offsetX offset-x
:offsetY offset-y
:offset-x offset-x
:offset-y offset-y
:blur blur
:spread spread
:hidden hidden

View File

@@ -7,7 +7,7 @@
(ns app.plugins.grid
(:require
[app.common.data :as d]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.shape.layout :as ctl]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.transforms :as dwt]
@@ -126,7 +126,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -141,7 +141,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnGap value)
(not (r/check-permission plugin-id "content:write"))
@@ -156,7 +156,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :verticalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -171,7 +171,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -186,7 +186,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :topPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -201,7 +201,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rightPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -216,7 +216,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :bottomPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -231,7 +231,7 @@
:set
(fn [_ value]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :leftPadding value)
(not (r/check-permission plugin-id "content:write"))
@@ -248,7 +248,7 @@
(u/display-not-valid :addRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRow-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -261,14 +261,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :addRowAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addRowAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addRowAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -285,7 +285,7 @@
(u/display-not-valid :addColumn-type type)
(and (or (= :percent type) (= :flex type) (= :lex type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -297,14 +297,14 @@
:addColumnAtIndex
(fn [index type value]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :addColumnAtIndex-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :addColumnAtIndex-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :addColumnAtIndex-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -317,7 +317,7 @@
:removeRow
(fn [index]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeRow index)
(not (r/check-permission plugin-id "content:write"))
@@ -329,7 +329,7 @@
:removeColumn
(fn [index]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :removeColumn index)
(not (r/check-permission plugin-id "content:write"))
@@ -342,14 +342,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :setColumn-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setColumn-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setColumn-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -362,14 +362,14 @@
(fn [index type value]
(let [type (keyword type)]
(cond
(not (us/safe-int? index))
(not (sm/valid-safe-int? index))
(u/display-not-valid :setRow-index index)
(not (contains? ctl/grid-track-types type))
(u/display-not-valid :setRow-type type)
(and (or (= :percent type) (= :flex type) (= :fixed type))
(not (us/safe-number? value)))
(not (sm/valid-safe-number? value)))
(u/display-not-valid :setRow-value value)
(not (r/check-permission plugin-id "content:write"))
@@ -393,10 +393,10 @@
(not (shape-proxy? child))
(u/display-not-valid :appendChild-child child)
(or (< row 0) (not (us/safe-int? row)))
(or (< row 0) (not (sm/valid-safe-int? row)))
(u/display-not-valid :appendChild-row row)
(or (< column 0) (not (us/safe-int? column)))
(or (< column 0) (not (sm/valid-safe-int? column)))
(u/display-not-valid :appendChild-column column)
(not (r/check-permission plugin-id "content:write"))
@@ -431,7 +431,7 @@
(let [cell (locate-cell self)
shape (u/proxy->shape self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :row-value value)
(nil? cell)
@@ -451,7 +451,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :rowSpan-value value)
(nil? cell)
@@ -471,7 +471,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :column-value value)
(nil? cell)
@@ -491,7 +491,7 @@
(let [shape (u/proxy->shape self)
cell (locate-cell self)]
(cond
(not (us/safe-int? value))
(not (sm/valid-safe-int? value))
(u/display-not-valid :columnSpan-value value)
(nil? cell)

View File

@@ -10,7 +10,7 @@
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.spec :as us]
[app.common.schema :as sm]
[app.common.types.color :as cc]
[app.common.uuid :as uuid]
[app.main.data.comments :as dc]
@@ -299,7 +299,7 @@
(fn [orientation value board]
(let [shape (u/proxy->shape board)]
(cond
(not (us/safe-number? value))
(not (sm/valid-safe-number? value))
(u/display-not-valid :addRulerGuide "Value not a safe number")
(not (contains? #{"vertical" "horizontal"} orientation))
@@ -345,8 +345,8 @@
(or (not (string? content)) (empty? content))
(u/display-not-valid :addCommentThread "Content not valid")
(or (not (us/safe-number? (:x position)))
(not (us/safe-number? (:y position))))
(or (not (sm/valid-safe-number? (:x position)))
(not (sm/valid-safe-number? (:y position))))
(u/display-not-valid :addCommentThread "Position not valid")
(and (some? board) (or (not (shape/shape-proxy? board)) (not (cfh/frame-shape? shape))))

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