mirror of
https://github.com/penpot/penpot.git
synced 2026-01-04 04:18:51 -05:00
Compare commits
3 Commits
juanfran-t
...
eva-replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bdcbde37c3 | ||
|
|
81c6cb52ca | ||
|
|
1dc274e6e2 |
21
.github/workflows/build-nitrate-module.yml
vendored
21
.github/workflows/build-nitrate-module.yml
vendored
@@ -1,21 +0,0 @@
|
||||
name: _NITRATE MODULE
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '36 5-20 * * 1-5'
|
||||
|
||||
jobs:
|
||||
build-bundle:
|
||||
uses: ./.github/workflows/build-bundle.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
build_wasm: "yes"
|
||||
build_storybook: "yes"
|
||||
|
||||
build-docker:
|
||||
needs: build-bundle
|
||||
uses: ./.github/workflows/build-docker.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "nitrate-module"
|
||||
1
.github/workflows/build-tag.yml
vendored
1
.github/workflows/build-tag.yml
vendored
@@ -23,7 +23,6 @@ jobs:
|
||||
|
||||
notify:
|
||||
name: Notifications
|
||||
runs-on: ubuntu-24.04
|
||||
needs: build-docker
|
||||
|
||||
steps:
|
||||
|
||||
@@ -8,17 +8,12 @@
|
||||
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Fix mask issues with component swap (by @dfelinto) [Github #7675](https://github.com/penpot/penpot/issues/7675)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
- Make i18n translation files load on-demand [Taiga #11474](https://tree.taiga.io/project/penpot/us/11474)
|
||||
|
||||
### :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)
|
||||
- Fix wrong board size presets in Android [Taiga #12339](https://tree.taiga.io/project/penpot/issue/12339)
|
||||
|
||||
|
||||
## 2.12.0 (Unreleased)
|
||||
@@ -82,7 +77,6 @@ example. It's still usable as before, we just removed the example.
|
||||
### :heart: Community contributions (Thank you!)
|
||||
|
||||
- Ensure consistent snap behavior across all zoom levels [Github #7774](https://github.com/penpot/penpot/pull/7774) by [@Tokytome](https://github.com/Tokytome)
|
||||
- Fix crash in token grid view due to tooltip validation (by @dfelinto) [Github #7887](https://github.com/penpot/penpot/pull/7887)
|
||||
|
||||
### :sparkles: New features & Enhancements
|
||||
|
||||
@@ -110,7 +104,6 @@ example. It's still usable as before, we just removed the example.
|
||||
- Fix input confirmation behavior is not uniform [Taiga #12294](https://tree.taiga.io/project/penpot/issue/12294)
|
||||
- Fix copy/pasting application/transit+json [Taiga #12721](https://tree.taiga.io/project/penpot/issue/12721)
|
||||
- Fix problem with plugins content attribute [Plugins #209](https://github.com/penpot/penpot-plugins/issues/209)
|
||||
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
|
||||
|
||||
## 2.11.1
|
||||
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||
export PENPOT_HOST=devenv
|
||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||
|
||||
export PENPOT_FLAGS="\
|
||||
$PENPOT_FLAGS \
|
||||
|
||||
@@ -2439,13 +2439,11 @@
|
||||
(ctk/get-swap-slot))
|
||||
(constantly false))
|
||||
|
||||
;; In the cases where the swapped shape was the first element of the masked group it would make the group to loose the
|
||||
;; mask property as part of the sanitization check on generate-delete-shapes, passing "ignore-mask" to prevent this
|
||||
[all-parents changes]
|
||||
(-> changes
|
||||
(cls/generate-delete-shapes
|
||||
file page objects (d/ordered-set (:id shape))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn :ignore-mask true}))
|
||||
{:allow-altering-copies true :ignore-children-fn ignore-swapped-fn}))
|
||||
[new-shape changes]
|
||||
(-> changes
|
||||
(generate-new-shape-for-swap shape file page libraries id-new-component index target-cell keep-props-values))]
|
||||
|
||||
@@ -123,10 +123,8 @@
|
||||
;; ignore-children-fn is used to ignore some descendants
|
||||
;; on the deletion process. It should receive a shape and
|
||||
;; return a boolean
|
||||
ignore-children-fn
|
||||
ignore-mask]
|
||||
:or {ignore-children-fn (constantly false)
|
||||
ignore-mask false}}]
|
||||
ignore-children-fn]
|
||||
:or {ignore-children-fn (constantly false)}}]
|
||||
(let [objects (pcb/get-objects changes)
|
||||
data (pcb/get-library-data changes)
|
||||
page-id (pcb/get-page-id changes)
|
||||
@@ -164,20 +162,18 @@
|
||||
lookup (d/getf objects)
|
||||
|
||||
groups-to-unmask
|
||||
(when-not ignore-mask
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
[])
|
||||
(reduce (fn [group-ids id]
|
||||
;; When the shape to delete is the mask of a masked group,
|
||||
;; the mask condition must be removed, and it must be
|
||||
;; converted to a normal group.
|
||||
(let [obj (lookup id)
|
||||
parent (lookup (:parent-id obj))]
|
||||
(if (and (:masked-group parent)
|
||||
(= id (first (:shapes parent))))
|
||||
(conj group-ids (:id parent))
|
||||
group-ids)))
|
||||
#{}
|
||||
ids-to-delete)
|
||||
|
||||
interacting-shapes
|
||||
(filter (fn [shape]
|
||||
|
||||
@@ -284,9 +284,9 @@
|
||||
(defn check-fn
|
||||
"Create a predefined check function"
|
||||
[s & {:keys [hint type code]}]
|
||||
(let [s (delay (schema s))
|
||||
validator* (delay (m/validator @s))
|
||||
explainer* (delay (m/explainer @s))
|
||||
(let [s (schema s)
|
||||
validator* (delay (m/validator s))
|
||||
explainer* (delay (m/explainer s))
|
||||
hint (or ^boolean hint "check error")
|
||||
type (or ^boolean type :assertion)
|
||||
code (or ^boolean code :data-validation)]
|
||||
@@ -304,7 +304,7 @@
|
||||
|
||||
(defn coercer
|
||||
[schema & {:as opts}]
|
||||
(let [decode-fn (lazy-decoder schema json-transformer)
|
||||
(let [decode-fn (decoder schema json-transformer)
|
||||
check-fn (check-fn schema opts)]
|
||||
(fn [data]
|
||||
(-> data decode-fn check-fn))))
|
||||
|
||||
@@ -14,8 +14,7 @@
|
||||
(defn parse
|
||||
[data]
|
||||
(cond
|
||||
(or (str/starts-with? data "%")
|
||||
(= data "develop"))
|
||||
(str/starts-with? data "%")
|
||||
{:full "develop"
|
||||
:branch "develop"
|
||||
:base "0.0.0"
|
||||
|
||||
@@ -395,8 +395,6 @@ COPY files/tmux.conf /root/.tmux.conf
|
||||
COPY files/sudoers /etc/sudoers
|
||||
|
||||
COPY files/Caddyfile /home/
|
||||
COPY files/selfsigned.crt /home/
|
||||
COPY files/selfsigned.key /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
|
||||
|
||||
@@ -33,8 +33,6 @@ services:
|
||||
- 3447:3447
|
||||
- 3448:3448
|
||||
- 3449:3449
|
||||
- 3449:3449/udp
|
||||
- 3450:3450
|
||||
- 6006:6006
|
||||
- 6060:6060
|
||||
- 6061:6061
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
{
|
||||
auto_https off
|
||||
}
|
||||
|
||||
localhost:3449 {
|
||||
reverse_proxy localhost:4449
|
||||
tls /home/selfsigned.crt /home/selfsigned.key
|
||||
}
|
||||
|
||||
http://localhost:3450 {
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
tls internal
|
||||
reverse_proxy localhost:4449
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
nginx
|
||||
caddy start -c /home/Caddyfile
|
||||
tail -f /dev/null;
|
||||
nginx;
|
||||
caddy run -c /home/Caddyfile;
|
||||
|
||||
@@ -38,11 +38,11 @@ http {
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_comp_level 3;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
|
||||
map $http_upgrade $connection_upgrade {
|
||||
default upgrade;
|
||||
@@ -223,15 +223,16 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
add_header Cache-Control "no-store";
|
||||
# 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIDuzCCAqOgAwIBAgIUa3THJQSn1+ErK65g1jDL0tjUkBYwDQYJKoZIhvcNAQEL
|
||||
BQAwXzELMAkGA1UEBhMCVVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
|
||||
bDEOMAwGA1UECgwFTG9jYWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxo
|
||||
b3N0MB4XDTI1MTIwMjA4MjUyM1oXDTI2MTIwMjA4MjUyM1owXzELMAkGA1UEBhMC
|
||||
VVMxDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2NhbDEOMAwGA1UECgwFTG9j
|
||||
YWwxDDAKBgNVBAsMA0RldjESMBAGA1UEAwwJbG9jYWxob3N0MIIBIjANBgkqhkiG
|
||||
9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyVIlfpIPE+QyL/q7IQOilEA7wEOZ6wbsh2Fr
|
||||
59H1gSLFvgoCxI6RVUkQ/MFRnw/r1ZbAqRpc2xAl5a9Ml14q20Zlj6dAHsWX6O2J
|
||||
EwNsD18dQmX3BncnjV3yCZM2iQcMFKuXG4KQNdIQNNvdIgtlrHYp0ohS9s3XC7cj
|
||||
KxNrm/pW9EAXfn9AYDd/qER090L2E4ipP9m/5l3MjinNc4l2kpH9rLOgb79H0RLt
|
||||
PK3/KP8ErZhAvzdmDBAdM5Z5K37b+TfB/kSVNUKL6qyw5CCjlShERLhBNprlnRfz
|
||||
tHNIQ1RHq3qJJN19ZnJrLqICuQ5ztvj7hBDiOSV0LnmyKgXr6wIDAQABo28wbTAd
|
||||
BgNVHQ4EFgQUPL8WGf6z/wB8TimJBx1zybsIeikwHwYDVR0jBBgwFoAUPL8WGf6z
|
||||
/wB8TimJBx1zybsIeikwDwYDVR0TAQH/BAUwAwEB/zAaBgNVHREEEzARgglsb2Nh
|
||||
bGhvc3SHBH8AAAEwDQYJKoZIhvcNAQELBQADggEBACMMVyR3kbNxnzuUc2lahKH4
|
||||
cPXVWOsvCvnDtjzm41XmKjUJTbtjn3p5d/ZmLbZ4zzIQULfWXO3XG/HevkvVo0g6
|
||||
6pJXTXc6C6ZhFG0rIYMcPPzmGmalDV5n+lUaCVx5XbFFxvRQ7893auwhRATdwGs+
|
||||
xiMyYbE2w9otKqyDItmJZJ5nW6vmXJ42YHxlXF18u9U88xqtOSMd5xZahbsmw7Gg
|
||||
A4/o4TPoAX5QfA306sL443WaczsF7bmsTf9qcYa/3xxQkP5Seyqx8ePWpS22qysE
|
||||
jG6XPpymxb6sb2mVaFBAzhEMb/eBvE9nRAopxmB7uV4TbqC51K/U3uo6jFX4Jbw=
|
||||
-----END CERTIFICATE-----
|
||||
@@ -1,28 +0,0 @@
|
||||
-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDJUiV+kg8T5DIv
|
||||
+rshA6KUQDvAQ5nrBuyHYWvn0fWBIsW+CgLEjpFVSRD8wVGfD+vVlsCpGlzbECXl
|
||||
r0yXXirbRmWPp0AexZfo7YkTA2wPXx1CZfcGdyeNXfIJkzaJBwwUq5cbgpA10hA0
|
||||
290iC2WsdinSiFL2zdcLtyMrE2ub+lb0QBd+f0BgN3+oRHT3QvYTiKk/2b/mXcyO
|
||||
Kc1ziXaSkf2ss6Bvv0fREu08rf8o/wStmEC/N2YMEB0zlnkrftv5N8H+RJU1Qovq
|
||||
rLDkIKOVKEREuEE2muWdF/O0c0hDVEereokk3X1mcmsuogK5DnO2+PuEEOI5JXQu
|
||||
ebIqBevrAgMBAAECggEABqtE+LNn8nW9v98jcc2IBjc2g4D5yVJaZYWxqGVJJ7T6
|
||||
Lfhw7Qf4AoZAHM9en9FMM7Ahw7hO2SboynoLJHyHGOp1FNQqiJptFNdBkjKr0rqI
|
||||
4pk0HK+3zLQO/4gz50gne0vP3qZtlorV5Jpf8e/Et3jWm9XOQcTB2e6AKL4k827B
|
||||
dv4Tld+/7PoZVXjahfrUWuIZr5mzyF1eUkD8sPOpdr3HJxSueqsOMjbG8XMRqCQ+
|
||||
5eCWWSW5yPQlMr7M7cXM+a0k73Xn1sKl7fP3/9byji25zxGUaMu5RA1kw0Oqseid
|
||||
RXuRxGphGZgnx1aFxDAPg3FtmGch7/Cc6WfqboOL0QKBgQD4GZO1gGaE8cg4lvuo
|
||||
ZUX2YJu6UJuNOmuhfvG3ui4WO9PHy3btc2q+3kutSuBcyIjhi+qbXasBcX/QOOJF
|
||||
udyTZc5PopNkJojS4JdXAZCiu5sKI3lp4DIt9qNISlXGgrJgdxGUO+DzarBctXdn
|
||||
BSwXFw5hcjJjl7wsPGQl1tBTQwKBgQDPuz5MEM5ZeUe9CT5sQDq/ld0u4aL5AHmx
|
||||
aaA2gzDgd9l2R5wHX6wLzjoVWXOmeqaYzJopt2JN4iXrtbjWkyePgZeZMyWoyJ/v
|
||||
clW9bi8HM9f9EpPr7czSj9sLUnsjd9cuTD+JuXK//jRGbRpw7r7nWtLHImjj6d2v
|
||||
APZRq0v2OQKBgBcESG/OObSbubeGSlKVEqiIzem7ELNJeDLDVCl3XE8zvbILbj0Z
|
||||
OA39EYhCKg5xjEFgeaNwTS0VGoZ2wIc3dv81sq4wpvvjl035CBFKU+DFBt0p7Vml
|
||||
MwKQnxVV0B9agLHyWe8mnvf2LeZr72ffUvfRa8QelA4pRYvVDnV0OF+BAoGAW6rM
|
||||
+tQPuvwB5DFIEozlX9XKHP4E5MyI5vktceDCmMtKcx92gup9CVif2Pv4ROaqzZK8
|
||||
FNyPzL6W7UTrpASb2H/fXgNsAudFbGyP2V/d8Ne34D1qeRoe4GwKxRxIqoYftpZ/
|
||||
E096i66pcsqCeINiSsWRbb6JesmgwbEzAScOBkECgYEA6O/Dibc9PaqRpaiE6Qut
|
||||
S3W/Rr1Pd1jbN4rOVI2TFCgMJQmc6jOdq2fCntR9acsa8HPx+djOlXTUBPKBZ/Ae
|
||||
p8umRdXVWcNMnwWVWHt7tsEuR/gYkxQ5xjXeS1VDPnEre9+EaevMBuVs8HdRsKQO
|
||||
uzvNGeAFEfqwIqn7CFQ+ndU=
|
||||
-----END PRIVATE KEY-----
|
||||
@@ -30,18 +30,13 @@ tmux select-window -t penpot:1
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch:app' enter
|
||||
|
||||
tmux new-window -t penpot:2 -n 'frontend ts'
|
||||
tmux new-window -t penpot:2 -n 'frontend storybook'
|
||||
tmux select-window -t penpot:2
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn watch:ts' enter
|
||||
|
||||
tmux new-window -t penpot:3 -n 'frontend storybook'
|
||||
tmux select-window -t penpot:3
|
||||
tmux send-keys -t penpot 'cd penpot/frontend' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch:storybook' enter
|
||||
|
||||
tmux new-window -t penpot:4 -n 'exporter'
|
||||
tmux select-window -t penpot:4
|
||||
tmux new-window -t penpot:3 -n 'exporter'
|
||||
tmux select-window -t penpot:3
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
tmux send-keys -t penpot 'rm -f target/app.js*' enter C-l
|
||||
tmux send-keys -t penpot 'yarn run watch' enter
|
||||
@@ -50,8 +45,8 @@ tmux split-window -v
|
||||
tmux send-keys -t penpot 'cd penpot/exporter' enter C-l
|
||||
tmux send-keys -t penpot './scripts/wait-and-start.sh' enter
|
||||
|
||||
tmux new-window -t penpot:5 -n 'backend'
|
||||
tmux select-window -t penpot:5
|
||||
tmux new-window -t penpot:4 -n 'backend'
|
||||
tmux select-window -t penpot:4
|
||||
tmux send-keys -t penpot 'cd penpot/backend' enter C-l
|
||||
tmux send-keys -t penpot './scripts/start-dev' enter
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ RUN set -ex; \
|
||||
useradd -U -M -u 1001 -s /bin/false -d /opt/penpot penpot; \
|
||||
mkdir -p /opt/data/assets; \
|
||||
chown -R penpot:penpot /opt/data; \
|
||||
mkdir -p /etc/nginx/overrides/main.d/; \
|
||||
mkdir -p /etc/nginx/overrides/http.d/; \
|
||||
mkdir -p /etc/nginx/overrides/server.d/; \
|
||||
mkdir -p /etc/nginx/overrides/location.d/;
|
||||
|
||||
@@ -42,11 +42,11 @@ http {
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_static on;
|
||||
gzip_comp_level 6;
|
||||
gzip_comp_level 4;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml application/wasm;
|
||||
gzip_types text/plain text/css text/javascript application/javascript application/json application/transit+json image/svg+xml;
|
||||
|
||||
proxy_buffer_size 16k;
|
||||
proxy_busy_buffers_size 24k; # essentially, proxy_buffer_size + 2 small buffers of 4k
|
||||
@@ -142,15 +142,24 @@ http {
|
||||
location / {
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
location ~ ^/js/config.js$ {
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
}
|
||||
|
||||
location ~* \.(js|css|jpg|svg|png|mjs|map)$ {
|
||||
add_header Cache-Control "max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
|
||||
}
|
||||
|
||||
location ~ ^/[^/]+/(.*)$ {
|
||||
return 301 " /404";
|
||||
}
|
||||
|
||||
add_header Last-Modified $date_gmt;
|
||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||
if_modified_since off;
|
||||
try_files $uri /index.html$is_args$args /index.html =404;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,19 +10,19 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/your-account">
|
||||
<h2>Your account →</h2>
|
||||
<p>Access your account settings and manage personal access tokens</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/teams">
|
||||
<h2>Teams →</h2>
|
||||
<p>Create and manage your teams</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/account-teams/comments/">
|
||||
<h2>Comments →</h2>
|
||||
<p>Give and receive feedback right over your designs</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/assets">
|
||||
<h2>Assets →</h2>
|
||||
<p>Store elements and styles to easily reuse them</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries">
|
||||
<h2>Libraries →</h2>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components">
|
||||
<h2>Components →</h2>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
<p>Speed your design workflow</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants">
|
||||
<h2>Variants →</h2>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens">
|
||||
<h2>Design Tokens →</h2>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -5,7 +5,7 @@ desc: Use Penpot's libraries for reusable design elements! Learn to create, mana
|
||||
---
|
||||
<h1 id="libraries">Libraries</h1>
|
||||
|
||||
<p class="main-paragraph">Libraries may include components, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
<p class="main-paragraph">Libraries may include components, graphics, colors and typographies. Learn how to create and manage them to better organize the pieces of your designs and speed your workflow.</p>
|
||||
|
||||
<h3 id="file-libraries">File libraries</h3>
|
||||
<p>Each file has its own file library which is where the assets that belong to this file are stored.</p>
|
||||
|
||||
@@ -10,31 +10,31 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/designing/workspace-basics">
|
||||
<h2>Workspace basics →</h2>
|
||||
<p>Get to know the Workspace, where designs are created</p>
|
||||
<p>Workspace basics</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers">
|
||||
<h2>Layers →</h2>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/color-stroke/">
|
||||
<h2>Color & Strokes→</h2>
|
||||
<p>Styling options available for each layer</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/text-typo">
|
||||
<h2>Text & Typography→</h2>
|
||||
<p>Styling text content & using custom fonts</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts">
|
||||
<h2>Flexible layouts →</h2>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/export-import/export-import-files/">
|
||||
<h2>Export/Import Penpot files →</h2>
|
||||
<p>How to export and import your Penpot files</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/export-import/exporting-layers/">
|
||||
<h2>Exporting layers →</h2>
|
||||
<p>How to export elements from your design into different file formats</p>
|
||||
<p>Exporting layers</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -16,7 +16,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/the-interface">
|
||||
<h2>Interface tour →</h2>
|
||||
<p>Take a tour of Penpot's main areas</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
@@ -28,7 +28,7 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/first-steps/info">
|
||||
<h2>Tutorials & info →</h2>
|
||||
<p>Useful resources to better understand Penpot</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -22,49 +22,49 @@ eleventyNavigation:
|
||||
<li>
|
||||
<a href="/user-guide/designing/layers/">
|
||||
<h2>Layers</h2>
|
||||
<p>Objects available in Penpot and how to get the most of them</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/designing/flexible-layouts/">
|
||||
<h2>Flexible layouts</h2>
|
||||
<p>Create designs that adapt automatically</p>
|
||||
<p>Create designs that adapt automatically.</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/components/">
|
||||
<h2>Components</h2>
|
||||
<p>Speed your design workflow with reusable components</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/variants/">
|
||||
<h2>Variants</h2>
|
||||
<p>Group components into a single, customizable one</p>
|
||||
<p>Penpot's main areas and features</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/design-tokens/">
|
||||
<h2>Design Tokens</h2>
|
||||
<p>Synchronize visual elements across your designs</p>
|
||||
<p>Penpot's main areas and features</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/dev-tools/#inspect-design">
|
||||
<h2>Inspect design</h2>
|
||||
<p>Get production-ready code</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping/">
|
||||
<h2>Prototyping</h2>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/design-systems/libraries/">
|
||||
<h2>Libraries</h2>
|
||||
<p>Organize and manage your stored elements with Libraries</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -10,13 +10,13 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/prototyping">
|
||||
<h2>Prototyping →</h2>
|
||||
<p>Build interactive prototypes to mimic your product behaviour</p>
|
||||
<p>Ways to start with Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/user-guide/prototyping-testing/testing-view-mode">
|
||||
<h2>Testing: View mode →</h2>
|
||||
<p>Test your designs and play the interactions</p>
|
||||
<p>Info of interest about Penpot</p>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
2
frontend/.gitignore
vendored
2
frontend/.gitignore
vendored
@@ -11,4 +11,4 @@ node_modules/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
/playwright/**/visual-specs/**/*.png
|
||||
/ts/dist/
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** @type { import('@storybook/react-vite').StorybookConfig } */
|
||||
const config = {
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)", "../ts/src/components/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"],
|
||||
staticDirs: ["../resources/public"],
|
||||
addons: [
|
||||
"@storybook/addon-themes",
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
|
||||
|
||||
import Components from "@target/components";
|
||||
import translations from "@public/translation.en.js";
|
||||
Components.setDefaultTranslations(translations);
|
||||
|
||||
import '../resources/public/css/ds.css';
|
||||
|
||||
export const decorators = [
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['ts/dist']),
|
||||
{
|
||||
files: ['ts/src/**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs['recommended-latest'],
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
@@ -17,8 +17,7 @@
|
||||
"@zip.js/zip.js@npm:^2.7.44": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
"@vitejs/plugin-react": "^4.2.0",
|
||||
"playwright": "1.52.0",
|
||||
"playwright-core": "1.52.0",
|
||||
"globals": "^16.5.0"
|
||||
"playwright-core": "1.52.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:app:assets": "node ./scripts/build-app-assets.js",
|
||||
@@ -33,9 +32,8 @@
|
||||
"e2e:server": "node ./scripts/e2e-server.js",
|
||||
"fmt:clj": "cljfmt fix --parallel=true src/ test/",
|
||||
"fmt:clj:check": "cljfmt check --parallel=false src/ test/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -c ts/src/**/*.ts -c --check",
|
||||
"lint:ts": "eslint ts/",
|
||||
"fmt:js": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js -w",
|
||||
"fmt:js:check": "yarn run prettier -c src/**/*.stories.jsx -c playwright/**/*.js -c scripts/**/*.js",
|
||||
"lint:clj": "clj-kondo --parallel --lint src/",
|
||||
"lint:scss": "yarn run prettier -c resources/styles -c src/**/*.scss",
|
||||
"lint:scss:fix": "yarn run prettier -c resources/styles -c src/**/*.scss -w",
|
||||
@@ -52,7 +50,6 @@
|
||||
"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:ts": "vite build --watch",
|
||||
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -62,16 +59,11 @@
|
||||
"@storybook/addon-vitest": "10.0.4",
|
||||
"@storybook/react-vite": "10.0.4",
|
||||
"@types/node": "^22.15.21",
|
||||
"@types/react": "^19.1.16",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"@vitest/browser": "3.2.4",
|
||||
"@vitest/coverage-v8": "3.2.4",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"concurrently": "^9.2.1",
|
||||
"esbuild": "^0.25.9",
|
||||
"eslint": "^9.36.0",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.22",
|
||||
"express": "^5.1.0",
|
||||
"fancy-log": "^2.0.0",
|
||||
"getopts": "^2.3.0",
|
||||
@@ -103,7 +95,6 @@
|
||||
"storybook": "10.0.4",
|
||||
"svg-sprite": "^2.0.4",
|
||||
"typescript": "^5.9.2",
|
||||
"typescript-eslint": "^8.45.0",
|
||||
"vite": "^6.3.5",
|
||||
"vitest": "^3.2.0",
|
||||
"wasm-pack": "^0.13.1",
|
||||
@@ -117,9 +108,7 @@
|
||||
"@penpot/plugins-runtime": "1.3.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.1",
|
||||
"@penpot/text-editor": "portal:./text-editor",
|
||||
"@penpot/ts": "portal:./ts",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@vitejs/plugin-react": "4.2.0",
|
||||
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",
|
||||
"compression": "^1.8.1",
|
||||
"date-fns": "^4.1.0",
|
||||
|
||||
@@ -73,7 +73,7 @@ export class BasePage {
|
||||
}
|
||||
|
||||
static async mockConfigFlags(page, flags) {
|
||||
const url = "**/js/config.js*";
|
||||
const url = "**/js/config.js";
|
||||
return await page.route(url, (route) =>
|
||||
route.fulfill({
|
||||
status: 200,
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 821 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 48 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 676 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 97 KiB |
@@ -17,26 +17,23 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
|
||||
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/ts-style.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
<link id="theme" href="css/main.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
{{#isDebug}}
|
||||
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="css/debug.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
{{/isDebug}}
|
||||
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
<script type="module">
|
||||
globalThis.penpotTranslations = JSON.parse({{& translations}});
|
||||
globalThis.penpotVersion = "%version%";
|
||||
globalThis.penpotBuildDate = "%buildDate%";
|
||||
</script>
|
||||
<!--cookie-consent-->
|
||||
</head>
|
||||
<body>
|
||||
@@ -49,7 +46,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& app_main}}";
|
||||
import { init } from "{{& main}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<link href="./css/ds.css?version={{& version}}" rel="stylesheet" type="text/css" />
|
||||
<link href="./css/ds.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
|
||||
|
||||
<style>
|
||||
body {
|
||||
@@ -9,3 +9,7 @@
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
window.penpotTranslations = JSON.parse({{& translations}});
|
||||
</script>
|
||||
|
||||
@@ -6,22 +6,22 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
window.penpotVersion = "%version%";
|
||||
window.penpotBuildDate = "%buildDate%";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script>window.penpotWorkerURI="{{& worker_main}}"</script>
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
|
||||
</head>
|
||||
<body>
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& rasterizer_main}}";
|
||||
import { init } from "{{& rasterizer}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -7,14 +7,12 @@
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
window.penpotVersion = "%version%";
|
||||
</script>
|
||||
|
||||
{{# manifest}}
|
||||
<script src="{{& config}}"></script>
|
||||
<script src="{{& polyfills}}"></script>
|
||||
<script type="importmap">{{& importmap }}</script>
|
||||
{{/manifest}}
|
||||
</head>
|
||||
<body>
|
||||
@@ -22,7 +20,7 @@
|
||||
{{# manifest}}
|
||||
<script type="module" src="{{& libs}}"></script>
|
||||
<script type="module">
|
||||
import { init } from "{{& render_main}}";
|
||||
import { init } from "{{& render}}";
|
||||
init();
|
||||
</script>
|
||||
{{/manifest}}
|
||||
|
||||
@@ -28,8 +28,6 @@ export function startWorker() {
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
|
||||
async function findFiles(basePath, predicate, options = {}) {
|
||||
predicate =
|
||||
@@ -49,7 +47,8 @@ async function findFiles(basePath, predicate, options = {}) {
|
||||
function syncDirs(originPath, destPath) {
|
||||
const command = `rsync -ar --delete ${originPath} ${destPath}`;
|
||||
|
||||
return new Promise((resolve, reject) => {proc.exec(command, (cause, stdout) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
proc.exec(command, (cause, stdout) => {
|
||||
if (cause) {
|
||||
reject(cause);
|
||||
} else {
|
||||
@@ -188,34 +187,37 @@ async function readManifestFile() {
|
||||
}
|
||||
|
||||
async function readShadowManifest() {
|
||||
const index = {
|
||||
app_main: "./js/main.js",
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
const ts = Date.now();
|
||||
try {
|
||||
const content = await readManifestFile();
|
||||
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
const index = {
|
||||
ts: ts,
|
||||
config: "./js/config.js",
|
||||
polyfills: "./js/polyfills.js",
|
||||
worker_main: "./js/worker/main.js",
|
||||
libs: "./js/libs.js",
|
||||
};
|
||||
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
}
|
||||
})
|
||||
};
|
||||
for (let item of content) {
|
||||
index[item.name] = "./js/" + item["output-name"] + "";
|
||||
}
|
||||
|
||||
return index;
|
||||
return index;
|
||||
} catch (cause) {
|
||||
const index = {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
@@ -255,7 +257,7 @@ const markedOptions = {
|
||||
|
||||
marked.use(markedOptions);
|
||||
|
||||
export async function compileTranslations() {
|
||||
async function readTranslations() {
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -292,10 +294,9 @@ export async function compileTranslations() {
|
||||
["uk", "ukr_UA"],
|
||||
"ha",
|
||||
];
|
||||
const result = {};
|
||||
|
||||
for (let lang of langs) {
|
||||
const result = {};
|
||||
|
||||
let filename = `${lang}.po`;
|
||||
if (l.isArray(lang)) {
|
||||
filename = `${lang[1]}.po`;
|
||||
@@ -314,6 +315,11 @@ export async function compileTranslations() {
|
||||
for (let key of Object.keys(trdata)) {
|
||||
if (key === "") continue;
|
||||
const comments = trdata[key].comments || {};
|
||||
|
||||
if (l.isNil(result[key])) {
|
||||
result[key] = {};
|
||||
}
|
||||
|
||||
const isMarkdown = l.includes(comments.flag, "markdown");
|
||||
|
||||
const msgs = trdata[key].msgstr;
|
||||
@@ -323,9 +329,9 @@ export async function compileTranslations() {
|
||||
message = marked.parseInline(message);
|
||||
}
|
||||
|
||||
result[key] = message;
|
||||
result[key][lang] = message;
|
||||
} else {
|
||||
result[key] = msgs.map((item) => {
|
||||
result[key][lang] = msgs.map((item) => {
|
||||
if (isMarkdown) {
|
||||
return marked.parseInline(item);
|
||||
} else {
|
||||
@@ -334,12 +340,22 @@ export async function compileTranslations() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
function filterTranslations(translations, langs = [], keyFilter) {
|
||||
const filteredEntries = Object.entries(translations)
|
||||
.filter(([translationKey, _]) => keyFilter(translationKey))
|
||||
.map(([translationKey, value]) => {
|
||||
const langEntries = Object.entries(value).filter(([lang, _]) =>
|
||||
langs.includes(lang),
|
||||
);
|
||||
return [translationKey, Object.fromEntries(langEntries)];
|
||||
});
|
||||
|
||||
return Object.fromEntries(filteredEntries);
|
||||
}
|
||||
|
||||
async function generateSvgSprite(files, prefix) {
|
||||
@@ -391,6 +407,14 @@ async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
let translations = await readTranslations();
|
||||
const storybookTranslations = JSON.stringify(
|
||||
filterTranslations(translations, ["en"], (key) =>
|
||||
key.startsWith("labels."),
|
||||
),
|
||||
);
|
||||
translations = JSON.stringify(translations);
|
||||
|
||||
const manifest = await readShadowManifest();
|
||||
let content;
|
||||
|
||||
@@ -412,16 +436,13 @@ async function generateTemplates() {
|
||||
"../public/images/sprites/assets.svg": assetsSprite,
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/index.mustache",
|
||||
context,
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
isDebug,
|
||||
},
|
||||
partials,
|
||||
);
|
||||
|
||||
@@ -429,30 +450,41 @@ async function generateTemplates() {
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/challenge.mustache",
|
||||
context,
|
||||
{},
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./resources/public/challenge.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-body.mustache",
|
||||
context,
|
||||
{
|
||||
manifest: manifest,
|
||||
},
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-body.html", content);
|
||||
|
||||
content = await renderTemplate(
|
||||
"resources/templates/preview-head.mustache",
|
||||
context,
|
||||
{
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(storybookTranslations),
|
||||
},
|
||||
partials,
|
||||
);
|
||||
await fs.writeFile("./.storybook/preview-head.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/render.mustache", context);
|
||||
content = await renderTemplate("resources/templates/render.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
|
||||
await fs.writeFile("./resources/public/render.html", content);
|
||||
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", context);
|
||||
content = await renderTemplate("resources/templates/rasterizer.mustache", {
|
||||
manifest: manifest,
|
||||
translations: JSON.stringify(translations),
|
||||
});
|
||||
|
||||
await fs.writeFile("./resources/public/rasterizer.html", content);
|
||||
}
|
||||
|
||||
@@ -38,7 +38,15 @@ fi
|
||||
yarn run build:app:libs || exit 1;
|
||||
yarn run build:app:assets || exit 1;
|
||||
|
||||
sed -i "s/render-wasm.js/render-wasm.js?version=$CURRENT_VERSION/g" ./resources/public/js/worker/main.js;
|
||||
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" ./resources/public/js/render_wasm.js;
|
||||
fi
|
||||
|
||||
rsync -avr resources/public/ target/dist/;
|
||||
|
||||
|
||||
@@ -4,6 +4,5 @@ await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import fs from "node:fs/promises";
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await fs.mkdir("resources/public/js", {recursive: true});
|
||||
|
||||
await h.compileStorybookStyles();
|
||||
await h.copyAssets();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
|
||||
@@ -1,19 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export JAVA_OPTS="\
|
||||
-Djava.util.logging.manager=org.apache.logging.log4j.jul.LogManager \
|
||||
-Djdk.attach.allowAttachSelf \
|
||||
-Dlog4j2.configurationFile=log4j2-devenv-repl.xml \
|
||||
-Djdk.tracePinnedThreads=full \
|
||||
-XX:+EnableDynamicAgentLoading \
|
||||
-XX:-OmitStackTraceInFastThrow \
|
||||
-XX:+UnlockDiagnosticVMOptions \
|
||||
-XX:+DebugNonSafepoints \
|
||||
--sun-misc-unsafe-memory-access=allow \
|
||||
--enable-preview \
|
||||
--enable-native-access=ALL-UNNAMED";
|
||||
|
||||
export OPTIONS="-A:dev"
|
||||
export OPTIONS="-A:dev -J-XX:-OmitStackTraceInFastThrow";
|
||||
|
||||
set -ex
|
||||
exec clojure $OPTIONS -M -e "$OPTIONS_EVAL" -m rebel-readline.main
|
||||
exec clojure $OPTIONS -M -m rebel-readline.main
|
||||
|
||||
@@ -52,7 +52,6 @@ await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
await h.compileTranslations();
|
||||
await h.compileSvgSprites();
|
||||
await h.compileTemplates();
|
||||
await h.compilePolyfills();
|
||||
@@ -82,7 +81,7 @@ h.watch("resources/templates", null, async function (path) {
|
||||
log.info("watch: translations (~)");
|
||||
h.watch("translations", null, async function (path) {
|
||||
log.info("changed:", path);
|
||||
await h.compileTranslations();
|
||||
await h.compileTemplates();
|
||||
});
|
||||
|
||||
log.info("watch: assets (~)");
|
||||
|
||||
@@ -23,28 +23,28 @@
|
||||
|
||||
:util-highlight
|
||||
{:entries [app.util.code-highlight]
|
||||
:depends-on #{:shared}}
|
||||
:depends-on #{:main}}
|
||||
|
||||
:main-auth
|
||||
{:entries [app.main.ui.auth
|
||||
app.main.ui.auth.verify-token]
|
||||
:depends-on #{:shared}}
|
||||
:depends-on #{:main}}
|
||||
|
||||
:main-viewer
|
||||
{:entries [app.main.ui.viewer]
|
||||
:depends-on #{:shared :main-auth}}
|
||||
:depends-on #{:main :main-auth}}
|
||||
|
||||
:main-workspace
|
||||
{:entries [app.main.ui.workspace]
|
||||
:depends-on #{:shared}}
|
||||
:depends-on #{:main}}
|
||||
|
||||
:main-dashboard
|
||||
{:entries [app.main.ui.dashboard]
|
||||
:depends-on #{:shared}}
|
||||
:depends-on #{:main}}
|
||||
|
||||
:main-settings
|
||||
{:entries [app.main.ui.settings]
|
||||
:depends-on #{:shared}}
|
||||
:depends-on #{:main}}
|
||||
|
||||
:render
|
||||
{:entries [app.render]
|
||||
|
||||
@@ -86,6 +86,7 @@
|
||||
(def default-theme "default")
|
||||
(def default-language "en")
|
||||
|
||||
(def translations (obj/get global "penpotTranslations"))
|
||||
(def themes (obj/get global "penpotThemes"))
|
||||
|
||||
(def build-date (parse-build-date global))
|
||||
@@ -190,4 +191,5 @@
|
||||
|
||||
(defn resolve-static-asset
|
||||
[path]
|
||||
(u/join public-uri path))
|
||||
(let [uri (u/join public-uri path)]
|
||||
(assoc uri :query (dm/str "version=" (:full version)))))
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
|
||||
(ns app.main
|
||||
(:require
|
||||
["@penpot/ts" :refer [setTranslation]]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.objects-map]
|
||||
@@ -39,8 +38,6 @@
|
||||
(log/setup! {:app :info})
|
||||
(log/set-level! :debug)
|
||||
|
||||
(setTranslation i18n/tr)
|
||||
|
||||
(when (= :browser cf/target)
|
||||
(log/inf :version (:full cf/version)
|
||||
:asserts *assert*
|
||||
@@ -95,7 +92,7 @@
|
||||
(defn ^:export init
|
||||
[]
|
||||
(mw/init!)
|
||||
(i18n/init)
|
||||
(i18n/init! cf/translations)
|
||||
(cur/init-styles)
|
||||
(thr/init!)
|
||||
(init-ui)
|
||||
@@ -117,4 +114,11 @@
|
||||
[]
|
||||
(reinit))
|
||||
|
||||
;; Reload the UI when the language changes
|
||||
(add-watch
|
||||
i18n/locale "locale"
|
||||
(fn [_ _ old-value current-value]
|
||||
(when (not= old-value current-value)
|
||||
(reinit))))
|
||||
|
||||
(set! (.-stackTraceLimit js/Error) 50)
|
||||
|
||||
@@ -148,17 +148,17 @@
|
||||
:width 768
|
||||
:height 1024}
|
||||
{:name "Google Pixel 7 Pro"
|
||||
:width 412
|
||||
:height 892}
|
||||
:width 1440
|
||||
:height 3120}
|
||||
{:name "Google Pixel 6a/6"
|
||||
:width 412
|
||||
:height 915}
|
||||
:width 1080
|
||||
:height 2400}
|
||||
{:name "Google Pixel 4a/5"
|
||||
:width 393
|
||||
:height 851}
|
||||
{:name "Samsung Galaxy S22"
|
||||
:width 360
|
||||
:height 780}
|
||||
:width 1080
|
||||
:height 2340}
|
||||
{:name "Samsung Galaxy S20+"
|
||||
:width 384
|
||||
:height 854}
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
(let [uagent (new ua/UAParser)]
|
||||
(merge
|
||||
{:app-version (:full cf/version)
|
||||
:locale i18n/*current-locale*}
|
||||
:locale @i18n/locale}
|
||||
(let [browser (.getBrowser uagent)]
|
||||
{:browser (obj/get browser "name")
|
||||
:browser-version (obj/get browser "version")})
|
||||
@@ -98,9 +98,7 @@
|
||||
(def context
|
||||
(atom (d/without-nils (collect-context))))
|
||||
|
||||
(add-watch i18n/state "events"
|
||||
(fn [_ _ _ v]
|
||||
(swap! context assoc :locale (get v :locale))))
|
||||
(add-watch i18n/locale ::events #(swap! context assoc :locale %4))
|
||||
|
||||
;; --- EVENT TRANSLATION
|
||||
|
||||
|
||||
@@ -53,16 +53,11 @@
|
||||
(assoc :profile-id id)
|
||||
(assoc :profile profile)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(->> (rx/from (i18n/set-locale (:lang profile)))
|
||||
(rx/ignore))))
|
||||
|
||||
ptk/EffectEvent
|
||||
(effect [_ state _]
|
||||
(let [profile (:profile state)]
|
||||
(swap! storage/user assoc :profile profile)
|
||||
(i18n/set-locale! (:lang profile))
|
||||
(plugins.register/init)))))
|
||||
|
||||
(def profile-fetched?
|
||||
|
||||
@@ -59,15 +59,9 @@
|
||||
"Parses `value` of a color `sd-token` into a map like `{:value 1 :unit \"px\"}`.
|
||||
If the value is not parseable and/or has missing references returns a map with `:errors`."
|
||||
[value]
|
||||
(let [missing-references (seq (cto/find-token-value-references value))]
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
(cond
|
||||
missing-references
|
||||
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
|
||||
:references missing-references}
|
||||
:else
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))))
|
||||
(if-let [tc (tinycolor/valid-color value)]
|
||||
{:value value :unit (tinycolor/color-format tc)}
|
||||
{:errors [(wte/error-with-value :error.token/invalid-color value)]}))
|
||||
|
||||
(defn- numeric-string? [s]
|
||||
(and (string? s)
|
||||
@@ -126,7 +120,7 @@
|
||||
If the `value` is not parseable and/or has missing references returns a map with `:errors`.
|
||||
If the `value` is parseable but is out of range returns a map with `warnings`."
|
||||
[value]
|
||||
(let [missing-references? (seq (seq (cto/find-token-value-references value)))
|
||||
(let [missing-references? (seq (cto/find-token-value-references value))
|
||||
parsed-value (cft/parse-token-value value)
|
||||
out-of-scope (not (<= 0 (:value parsed-value) 1))
|
||||
references (seq (cto/find-token-value-references value))]
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.transit :as t]
|
||||
[app.common.types.component :as ctc]
|
||||
[app.common.types.components-list :as ctkl]
|
||||
[app.common.types.shape :as cts]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.main.data.changes :as dch]
|
||||
[app.main.data.comments :as dcmt]
|
||||
@@ -553,6 +551,7 @@
|
||||
component-id (:component-id shape)
|
||||
undo-id (js/Symbol)]
|
||||
|
||||
|
||||
(when valid?
|
||||
(if (ctc/is-variant-container? shape)
|
||||
;; Rename the full variant when it is a variant container
|
||||
@@ -567,43 +566,6 @@
|
||||
(dwl/rename-component component-id clean-name))
|
||||
(dwu/commit-undo-transaction undo-id))))))))))
|
||||
|
||||
(defn rename-shape-or-variant
|
||||
([id name]
|
||||
(rename-shape-or-variant nil nil id name))
|
||||
([file-id page-id id name]
|
||||
(ptk/reify ::rename-shape-or-variant
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [file-id (d/nilv file-id (:current-file-id state))
|
||||
page-id (d/nilv page-id (:current-page-id state))
|
||||
|
||||
file-data (dsh/lookup-file-data state file-id)
|
||||
shape
|
||||
(-> (dsh/lookup-page-objects state file-id page-id)
|
||||
(get id))
|
||||
|
||||
is-variant? (ctc/is-variant? shape)
|
||||
variant-id (when is-variant? (:variant-id shape))
|
||||
variant-name (when is-variant? (:variant-name shape))
|
||||
component-id (:component-id shape)
|
||||
component (ctkl/get-component file-data (:component-id shape))
|
||||
variant-properties (:variant-properties component)]
|
||||
(cond
|
||||
(and variant-name (ctv/valid-properties-formula? name))
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id))
|
||||
|
||||
variant-name
|
||||
(rx/of (dwva/update-properties-names-and-values
|
||||
component-id variant-id variant-properties {})
|
||||
(dwva/remove-empty-properties variant-id)
|
||||
(dwva/update-error component-id name))
|
||||
|
||||
:else
|
||||
(rx/of (end-rename-shape id name))))))))
|
||||
|
||||
;; --- Update Selected Shapes attrs
|
||||
|
||||
(defn update-selected-shapes
|
||||
|
||||
@@ -128,16 +128,14 @@
|
||||
related-components (cfv/find-variant-components data objects variant-id)
|
||||
|
||||
props (-> related-components last :variant-properties)
|
||||
valid-pos? (> (count props) pos)
|
||||
prop-name (when valid-pos? (-> props (nth pos) :name))
|
||||
prop-name (-> props (nth pos) :name)
|
||||
|
||||
changes (when valid-pos?
|
||||
(-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name)))
|
||||
changes (-> (pcb/empty-changes it page-id)
|
||||
(pcb/with-objects objects)
|
||||
(pcb/with-library-data data)
|
||||
(clvp/generate-update-property-name variant-id pos new-name))
|
||||
undo-id (js/Symbol)]
|
||||
(when (and valid-pos? (not= prop-name new-name))
|
||||
(when (not= prop-name new-name)
|
||||
(rx/of
|
||||
(dwu/start-undo-transaction undo-id)
|
||||
(dch/commit-changes changes)
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.main.ui.components.dropdown :refer [dropdown-content*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as ug]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as tm]
|
||||
@@ -54,13 +53,14 @@
|
||||
(def ^:private valid-option?
|
||||
(sm/lazy-validator schema:option))
|
||||
|
||||
(mf/defc context-menu-inner*
|
||||
[{:keys [on-close options selectable selected
|
||||
(mf/defc context-menu*
|
||||
[{:keys [show on-close options selectable selected
|
||||
top left fixed min-width origin width]
|
||||
:as props}]
|
||||
|
||||
(assert (every? valid-option? options) "expected valid options")
|
||||
(assert (fn? on-close) "missing `on-close` prop")
|
||||
(assert (boolean? show) "missing `show` prop")
|
||||
(assert (vector? options) "missing `options` prop")
|
||||
|
||||
(let [width (d/nilv width "initial")
|
||||
@@ -80,15 +80,14 @@
|
||||
offset-x (get state :offset-x)
|
||||
offset-y (get state :offset-y)
|
||||
levels (get state :levels)
|
||||
internal-id (mf/use-id)
|
||||
|
||||
on-local-close
|
||||
(mf/use-fn
|
||||
(mf/deps on-close)
|
||||
(fn []
|
||||
(swap! state* assoc :levels [{:parent nil :options options}])
|
||||
(when (fn? on-close)
|
||||
(on-close))))
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}])
|
||||
(on-close)))
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-close on-local-close})
|
||||
@@ -217,22 +216,11 @@
|
||||
(swap! state* assoc :levels [{:parent nil
|
||||
:options options}]))
|
||||
|
||||
(mf/with-effect [internal-id]
|
||||
(ug/dispatch! (ug/event "penpot:context-menu:open" #js {:id internal-id})))
|
||||
|
||||
(mf/with-effect [internal-id on-local-close]
|
||||
(letfn [(on-event [event]
|
||||
(when-let [detail (unchecked-get event "detail")]
|
||||
(when (not= internal-id (unchecked-get detail "id"))
|
||||
(on-local-close event))))]
|
||||
(ug/listen "penpot:context-menu:open" on-event)
|
||||
(partial ug/unlisten "penpot:context-menu:open" on-event)))
|
||||
|
||||
(mf/with-effect [ids]
|
||||
(tm/schedule-on-idle
|
||||
#(dom/focus! (dom/get-element (first ids)))))
|
||||
|
||||
(when (some? levels)
|
||||
(when (and show (some? levels))
|
||||
[:> dropdown-content* props
|
||||
(let [level (peek levels)
|
||||
options (:options level)
|
||||
@@ -241,7 +229,7 @@
|
||||
[:div {:class (stl/css-case
|
||||
:is-selectable selectable
|
||||
:context-menu true
|
||||
:is-open true
|
||||
:is-open show
|
||||
:fixed fixed)
|
||||
:style {:top (+ top offset-y)
|
||||
:left (+ left offset-x)}
|
||||
@@ -253,7 +241,7 @@
|
||||
:role "menu"
|
||||
:ref check-menu-offscreen}
|
||||
|
||||
(when parent
|
||||
(when-let [parent (:parent level)]
|
||||
[:*
|
||||
[:li {:id "go-back-sub-option"
|
||||
:class (stl/css :context-menu-item)
|
||||
@@ -268,7 +256,7 @@
|
||||
|
||||
[:li {:class (stl/css :separator)}]])
|
||||
|
||||
(for [[index option] (d/enumerate options)]
|
||||
(for [[index option] (d/enumerate (:options level))]
|
||||
(let [name (:name option)
|
||||
id (:id option)
|
||||
sub-options (:options option)
|
||||
@@ -309,12 +297,3 @@
|
||||
:data-testid id}
|
||||
name
|
||||
[:span {:class (stl/css :submenu-icon)} deprecated-icon/arrow]])]))))]])])))
|
||||
|
||||
(mf/defc context-menu*
|
||||
{::mf/private true}
|
||||
[{:keys [show] :as props}]
|
||||
|
||||
(assert (boolean? show) "expected `show` prop to be a boolean")
|
||||
|
||||
(when ^boolean show
|
||||
[:> context-menu-inner* props]))
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
(ns app.main.ui.dashboard.projects
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
["@penpot/ts" :refer [TestTsxComponent]]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.time :as ct]
|
||||
[app.main.data.common :as dcm]
|
||||
@@ -51,11 +50,8 @@
|
||||
::mf/props :obj
|
||||
::mf/private true}
|
||||
[{:keys [can-edit]}]
|
||||
;; (js/console.log "xxxx" TestTsxComponent)
|
||||
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
|
||||
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
||||
[:div
|
||||
[:> TestTsxComponent]]
|
||||
[:div#dashboard-projects-title {:class (stl/css :dashboard-title)}
|
||||
[:h1 (tr "dashboard.projects-title")]]
|
||||
(when can-edit
|
||||
|
||||
@@ -151,16 +151,14 @@
|
||||
|
||||
(mf/defc menu-team-icon*
|
||||
[{:keys [subscription-type]}]
|
||||
[:span {:class (stl/css :subscription-icon-wrapper)}
|
||||
[:> icon* {:icon-id (case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)
|
||||
:class (stl/css :subscription-icon)
|
||||
:size "s"
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}]])
|
||||
[:span {:class (stl/css :subscription-icon)
|
||||
:title (if (= subscription-type "unlimited")
|
||||
(tr "subscription.dashboard.power-up.unlimited-plan")
|
||||
(tr "subscription.dashboard.power-up.enterprise-plan"))
|
||||
:data-testid "subscription-icon"}
|
||||
(case subscription-type
|
||||
"unlimited" i/character-u
|
||||
"enterprise" i/character-e)])
|
||||
|
||||
(mf/defc main-menu-power-up*
|
||||
[{:keys [close-sub-menu]}]
|
||||
|
||||
@@ -144,20 +144,20 @@
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.subscription-icon-wrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
background: var(--color-background-primary);
|
||||
border-radius: $br-6;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
block-size: var(--sp-xl);
|
||||
inline-size: var(--sp-xl);
|
||||
}
|
||||
|
||||
.subscription-icon {
|
||||
@extend .button-icon;
|
||||
background: var(--color-background-primary);
|
||||
stroke: var(--color-foreground-secondary);
|
||||
border-radius: 6px;
|
||||
border: 1.75px solid var(--color-foreground-secondary);
|
||||
stroke-width: 2.25px;
|
||||
width: var(--sp-xl);
|
||||
height: var(--sp-xl);
|
||||
|
||||
svg {
|
||||
block-size: var(--sp-m);
|
||||
inline-size: var(--sp-m);
|
||||
}
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
(ns app.main.ui.ds
|
||||
(:require
|
||||
[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*]]
|
||||
@@ -43,6 +44,8 @@
|
||||
[app.util.i18n :as i18n]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(i18n/init! cf/translations)
|
||||
|
||||
(def default
|
||||
"A export used for storybook"
|
||||
(mf/object
|
||||
@@ -77,11 +80,6 @@
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
(i18n/set-translations "en" data))
|
||||
|
||||
;; meta / misc
|
||||
:meta
|
||||
{:icons (clj->js (sort icon-list))
|
||||
|
||||
@@ -173,7 +173,7 @@
|
||||
[:id {:optional true} :string]
|
||||
[:offset {:optional true} :int]
|
||||
[:delay {:optional true} :int]
|
||||
[:content [:or fn? :string map?]]
|
||||
[:content [:or fn? :string]]
|
||||
[:placement {:optional true}
|
||||
[:maybe [:enum "top" "bottom" "left" "right" "top-right" "bottom-right" "bottom-left" "top-left"]]]])
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@
|
||||
[app.main.ui.releases.v2-1]
|
||||
[app.main.ui.releases.v2-10]
|
||||
[app.main.ui.releases.v2-11]
|
||||
[app.main.ui.releases.v2-12]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.main.ui.releases.v2-4]
|
||||
@@ -103,4 +102,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.12")))
|
||||
(rc/render-release-notes (assoc params :version "2.11")))
|
||||
|
||||
@@ -1,162 +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.releases.v2-12
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.12"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-slide-0.jpg"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot 2.12 is here!"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Better tokens visibility and more!"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This release focuses on making your everyday workflow feel clearer, faster and more intuitive. Tokens are now easier to see and apply, appearing directly where you work and giving the designs better context during code inspection. Variants gain a more natural flow thanks to simple boolean toggles that remove friction when switching states. And PDF export becomes more flexible, letting you choose exactly which boards to share so your files match the story you want to tell."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Together, these enhancements bring greater control and fluidity to your entire design process."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-tokens-sidebar.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Better tokens visibility"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Better tokens visibility"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Design systems should be both powerful and effortless to use. This release brings tokens closer to where you work, making them easier to apply and easier to understand."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Apply color tokens right from the sidebar"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Your color tokens now appear directly in the properties sidebar, making it faster to apply or unapply tokens from the design tab. No more digging: now you can use tokens within your design flow."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"See token names in the Inspect panel"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Developers now get a clearer context during handoff. The Inspect panel shows the actual token used in your design, in a similar way to how styles are displayed. This small detail reduces ambiguity, aligns everyone on the same language, and strengthens collaboration across the team."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-variants.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Simpler boolean variants"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Simpler boolean variants"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Variants are central to building flexible, scalable components. With this release, boolean properties become far easier to work with."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"A simple toggle for boolean values"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Binary states now use a clean toggle, to be able to switch visually, instead of a dropdown. This makes adjusting component states more intuitive and speeds up working with multiple instances."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"It’s a subtle improvement, but it removes friction you feel hundreds of times a week, and makes component work flow more naturally."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
|
||||
2
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.12-export-pdf.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Smarter PDF export"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Smarter PDF export"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Exporting your work is now more precise and flexible."]
|
||||
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"Select specific boards when exporting"]
|
||||
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"You’re now in control of which boards make it into your PDF. Share just the final screens, just a flow, just the workshop materials. This streamlined export flow adapts to the way real teams work: share the story you want to tell, with exactly the boards you need."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: deprecated.$s-324 1fr;
|
||||
height: deprecated.$s-500;
|
||||
width: deprecated.$s-888;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: deprecated.$s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: deprecated.$s-324;
|
||||
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: deprecated.$s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr deprecated.$s-32;
|
||||
gap: deprecated.$s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include deprecated.flexCenter;
|
||||
@include deprecated.headlineSmallTypography;
|
||||
height: deprecated.$s-32;
|
||||
width: deprecated.$s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-16;
|
||||
width: deprecated.$s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: deprecated.$s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -35,7 +35,7 @@
|
||||
[app.main.ui.workspace.tokens.export.modal]
|
||||
[app.main.ui.workspace.tokens.import]
|
||||
[app.main.ui.workspace.tokens.import.modal]
|
||||
[app.main.ui.workspace.tokens.management.forms.modals]
|
||||
[app.main.ui.workspace.tokens.management.create.modals]
|
||||
[app.main.ui.workspace.tokens.settings]
|
||||
[app.main.ui.workspace.tokens.themes.create-modal]
|
||||
[app.main.ui.workspace.viewport :refer [viewport*]]
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.variant :as ctv]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.variants :as dwv]
|
||||
[app.main.store :as st]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
@@ -68,7 +69,15 @@
|
||||
name (str/trim (dom/get-value name-input))]
|
||||
(on-stop-edit)
|
||||
(reset! edition* false)
|
||||
(st/emit! (dw/rename-shape-or-variant shape-id name)))))
|
||||
(if variant-name
|
||||
(if (ctv/valid-properties-formula? name)
|
||||
(st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties (ctv/properties-formula->map name))
|
||||
(dwv/remove-empty-properties variant-id)
|
||||
(dwv/update-error component-id))
|
||||
(st/emit! (dwv/update-properties-names-and-values component-id variant-id variant-properties {})
|
||||
(dwv/remove-empty-properties variant-id)
|
||||
(dwv/update-error component-id name)))
|
||||
(st/emit! (dw/end-rename-shape shape-id name))))))
|
||||
|
||||
cancel-edit
|
||||
(mf/use-fn
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.generic-form
|
||||
(ns app.main.ui.workspace.tokens.management.create.border-radius
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
@@ -23,8 +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.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [default-validate-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]]
|
||||
@@ -39,25 +38,8 @@
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
|
||||
(defn get-value-for-validator
|
||||
[active-tab value value-subfield form-type]
|
||||
|
||||
(case form-type
|
||||
:indexed
|
||||
(if (= active-tab :reference)
|
||||
(:reference value)
|
||||
(value-subfield value))
|
||||
|
||||
:composite
|
||||
(if (= active-tab :reference)
|
||||
(get value :reference)
|
||||
value)
|
||||
|
||||
value))
|
||||
|
||||
(defn- default-make-schema
|
||||
[tokens-tree _]
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
@@ -80,37 +62,14 @@
|
||||
(nil? (cto/token-value-self-reference? name value))))]]))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token
|
||||
validator
|
||||
action
|
||||
is-create
|
||||
selected-token-set-id
|
||||
tokens-tree-in-selected-set
|
||||
token-type
|
||||
make-schema
|
||||
input-component
|
||||
initial
|
||||
type
|
||||
value-subfield
|
||||
input-value-placeholder] :as props}]
|
||||
[{:keys [token validate-token action is-create selected-token-set-id tokens-tree-in-selected-set] :as props}]
|
||||
|
||||
(let [make-schema (or make-schema default-make-schema)
|
||||
input-component (or input-component token.controls/input*)
|
||||
validate-token (or validator default-validate-token)
|
||||
|
||||
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
|
||||
active-tab (deref active-tab*)
|
||||
|
||||
on-toggle-tab
|
||||
(mf/use-fn
|
||||
(mf/deps)
|
||||
(fn [new-tab]
|
||||
(let [new-tab (keyword new-tab)]
|
||||
(reset! active-tab* new-tab))))
|
||||
|
||||
token
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token {:type token-type}))
|
||||
(or token {:type :border-radius}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
@@ -130,15 +89,14 @@
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||
(make-schema tokens-tree-in-selected-set active-tab))
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
(or initial
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")}))
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
@@ -178,13 +136,12 @@
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type value-subfield type active-tab)
|
||||
(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])
|
||||
value-for-validation (get-value-for-validator active-tab value value-subfield type)]
|
||||
(->> (validate-token {:token-value value-for-validation
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
@@ -230,16 +187,12 @@
|
||||
{:level :warning :appearance :ghost} (tr "workspace.tokens.warning-name-change")]])]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-placeholder (tr "workspace.tokens.token-value-enter"))
|
||||
[:> input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:form form
|
||||
:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:subfield value-subfield
|
||||
:toggle on-toggle-tab}]]
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
@@ -8,16 +8,18 @@
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.inputs-wrapper {
|
||||
.form-wrapper {
|
||||
width: $sz-384;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
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);
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
@@ -28,9 +30,29 @@
|
||||
grid-template-columns: 1fr auto;
|
||||
}
|
||||
|
||||
.title {
|
||||
@include t.use-typography("body-small");
|
||||
.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;
|
||||
}
|
||||
@@ -0,0 +1,227 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.color
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.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.color-input-token :refer [color-input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/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 {:error/fn token-value-error-fn}]]
|
||||
[:color-result {:optional true} ::sm/any]
|
||||
|
||||
[: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]
|
||||
(or token {:type :color}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(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)}
|
||||
[:> color-input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.color-input
|
||||
(ns app.main.ui.workspace.tokens.management.create.color-input-token
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.colors :as color]
|
||||
@@ -16,7 +16,7 @@
|
||||
[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 :as ds]
|
||||
[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]
|
||||
@@ -28,30 +28,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Color Inputs -------------------------------------------------------------
|
||||
;;
|
||||
;; The color input exists in two variants: the normal (primitive) input and the
|
||||
;; indexed input. Both support hex, rgb(), hsl(), named colors, opacity changes,
|
||||
;; color-picker selection, and token references.
|
||||
;;
|
||||
;; 1) Normal Color Input (primitive)
|
||||
;; - Used when the token’s `:value` *is a single color* or a reference.
|
||||
;; - Writes directly to `:value`.
|
||||
;; - Validation ensures the color is in an accepted format or is a valid
|
||||
;; color token reference.
|
||||
|
||||
;; 2) Indexed Color Input
|
||||
;; - Used when the token’s value stores an array of items (e.g. inside
|
||||
;; shadows, where each shadow layer has its own :color field).
|
||||
;; - The input writes to a nested subfield:
|
||||
;; [:value <subfield> <index> :color]
|
||||
;; - Only that specific color entry is validated.
|
||||
;; - Other properties (offsets, blur, inset, etc.) remain untouched.
|
||||
;;
|
||||
;; Both variants provide identical color-picker and text-input behavior, but
|
||||
;; differ in how they persist the value within the form’s nested structure.
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
@@ -126,7 +102,7 @@
|
||||
:on-finish-drag on-finish-drag
|
||||
:on-change on-change'}]]))
|
||||
|
||||
(mf/defc color-input*
|
||||
(mf/defc color-input-token*
|
||||
[{:keys [name tokens token] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
@@ -277,39 +253,39 @@
|
||||
(rx/dispose! subs))))
|
||||
|
||||
[:*
|
||||
[:> ds/input* props]
|
||||
[:> input* props]
|
||||
|
||||
(when color-ramp-open?
|
||||
[:> ramp* {:color value :on-change on-change-value}])]))
|
||||
|
||||
(defn- on-indexed-input-change
|
||||
([form field index value value-subfield]
|
||||
(on-indexed-input-change form field index value value-subfield false))
|
||||
([form field index value value-subfield trim?]
|
||||
(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 value-subfield index field] (if trim? (str/trim value) value))
|
||||
(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 indexed-color-input*
|
||||
[{:keys [name tokens token index value-subfield] :rest props}]
|
||||
(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 value-subfield index input-name])
|
||||
(get-in @form [:errors :value composite-type index input-name])
|
||||
|
||||
value
|
||||
(get-in @form [:data :value value-subfield index input-name] "")
|
||||
(get-in @form [:data :value composite-type index input-name] "")
|
||||
|
||||
color-resolved
|
||||
(get-in @form [:data :value value-subfield index :color-result] "")
|
||||
(get-in @form [:data :value composite-type index :color-result] "")
|
||||
|
||||
valid-color (or (tinycolor/valid-color value)
|
||||
(tinycolor/valid-color color-resolved))
|
||||
@@ -333,7 +309,7 @@
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if-let [value (get-in token [:value value-subfield index input-name])]
|
||||
(if-let [value (get-in token [:value composite-type index input-name])]
|
||||
(rx/behavior-subject value)
|
||||
(rx/subject)))
|
||||
|
||||
@@ -387,7 +363,7 @@
|
||||
(tinycolor/set-alpha (or alpha 1))
|
||||
(tinycolor/->string format))]
|
||||
(when (not= value color-value)
|
||||
(on-indexed-input-change form input-name index color-value value-subfield true)
|
||||
(on-composite-indexed-input-token-change form input-name index color-value composite-type true)
|
||||
(rx/push! resolve-stream color-value)))))
|
||||
|
||||
on-change
|
||||
@@ -398,7 +374,7 @@
|
||||
value (if (tinycolor/hex-without-hash-prefix? raw-value)
|
||||
(dm/str "#" raw-value)
|
||||
raw-value)]
|
||||
(on-indexed-input-change form input-name index value value-subfield true)
|
||||
(on-composite-indexed-input-token-change form input-name index value composite-type true)
|
||||
(rx/push! resolve-stream value))))
|
||||
|
||||
props
|
||||
@@ -414,7 +390,7 @@
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield]
|
||||
(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))
|
||||
@@ -428,24 +404,24 @@
|
||||
(cond
|
||||
(and error (str/empty? (:error/value error)))
|
||||
(do
|
||||
(swap! form update-in [:errors :value value-subfield index] dissoc input-name)
|
||||
(swap! form update-in [:data :value value-subfield index] dissoc input-name)
|
||||
(swap! form assoc-in [:data :value value-subfield index :color-result] "")
|
||||
(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 value-subfield index input-name] {:message error'})
|
||||
(swap! form assoc-in [:data :value value-subfield index :color-result] "")
|
||||
(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 value-subfield index input-name] "")]
|
||||
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 value-subfield index :color-result] (dwtf/format-token-value 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"})))))))]
|
||||
@@ -453,7 +429,7 @@
|
||||
(rx/dispose! subs))))
|
||||
|
||||
[:*
|
||||
[:> ds/input* props]
|
||||
[:> input* props]
|
||||
|
||||
(when color-ramp-open?
|
||||
[:> ramp* {:color value :on-change on-change-value}])]))
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox
|
||||
(ns app.main.ui.workspace.tokens.management.create.combobox-token-fonts
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
@@ -24,30 +24,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Font Picker Inputs -------------------------------------------------------
|
||||
;;
|
||||
;; We provide two versions of the font picker: the normal (primitive) input and
|
||||
;; the composite input. Both allow selecting a font-family either by typing free
|
||||
;; text or by choosing from the list of fonts loaded in the application. Comma-
|
||||
;; separated values are treated as literal font-family lists.
|
||||
;;
|
||||
;; 1) Normal Font Picker (primitive)
|
||||
;; - Used when the token’s value *is itself* a font-family (string) or a
|
||||
;; reference to another token.
|
||||
;; - The input writes directly to `:value`.
|
||||
;; - Validation ensures the string is a valid font-family or a valid token
|
||||
;; reference.
|
||||
;;
|
||||
;; 2) Composite Font Picker
|
||||
;; - Used inside typography tokens, where `:value` is a map (e.g. contains
|
||||
;; :font-family, :font-weight, :letter-spacing, etc.).
|
||||
;; - The input writes to the specific subfield `[:value :font-family]`.
|
||||
;; - Only this field is validated and updated—other typography fields remain
|
||||
;; untouched.
|
||||
;;
|
||||
;; Both modes share the same UI behaviour, but differ in how they store and
|
||||
;; validate data within the form state.
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
@@ -69,7 +45,7 @@
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc fonts-combobox*
|
||||
(mf/defc font-picker-combobox*
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
@@ -196,7 +172,7 @@
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
(mf/defc composite-fonts-combobox*
|
||||
(mf/defc font-picker-composite-combobox*
|
||||
[{:keys [token tokens name] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
@@ -0,0 +1,225 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.create.dimensions
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.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.input-token :refer [input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/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 {:error/fn token-value-error-fn}]]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(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]
|
||||
(or token {:type :dimensions}))
|
||||
|
||||
token-type
|
||||
(get token :type)
|
||||
|
||||
token-properties
|
||||
(dwta/get-token-properties token)
|
||||
|
||||
token-title (str/lower (:title token-properties))
|
||||
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
;; even if the name has been overriden by a token with the same name
|
||||
;; in another set below.
|
||||
(cond-> tokens
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set]
|
||||
(make-schema tokens-tree-in-selected-set))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
{:name (:name token "")
|
||||
:value (:value token "")
|
||||
:description (:description token "")})
|
||||
|
||||
form
|
||||
(fm/use-form :schema schema
|
||||
:initial initial)
|
||||
|
||||
warning-name-change?
|
||||
(not= (get-in @form [:data :name])
|
||||
(:name initial))
|
||||
|
||||
on-cancel
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)))
|
||||
|
||||
on-delete-token
|
||||
(mf/use-fn
|
||||
(mf/deps selected-token-set-id token)
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
(modal/hide!)
|
||||
(st/emit! (dwtl/delete-token selected-token-set-id (:id token)))))
|
||||
|
||||
handle-key-down-delete
|
||||
(mf/use-fn
|
||||
(mf/deps on-delete-token)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-delete-token e))))
|
||||
|
||||
handle-key-down-cancel
|
||||
(mf/use-fn
|
||||
(mf/deps on-cancel)
|
||||
(fn [e]
|
||||
(when (or (k/enter? e) (k/space? e))
|
||||
(on-cancel e))))
|
||||
|
||||
on-submit
|
||||
(mf/use-fn
|
||||
(mf/deps validate-token token tokens token-type)
|
||||
(fn [form _event]
|
||||
(let [name (get-in @form [:clean-data :name])
|
||||
description (get-in @form [:clean-data :description])
|
||||
value (get-in @form [:clean-data :value])]
|
||||
(->> (validate-token {:token-value value
|
||||
:token-name name
|
||||
:token-description description
|
||||
:prev-token token
|
||||
:tokens tokens})
|
||||
(rx/subs!
|
||||
(fn [valid-token]
|
||||
(st/emit!
|
||||
(if is-create
|
||||
(dwtl/create-token (ctob/make-token {:name name
|
||||
:type token-type
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
|
||||
(dwtl/update-token (:id token)
|
||||
{:name name
|
||||
:value (:value valid-token)
|
||||
:description description}))
|
||||
(dwtp/propagate-workspace-tokens)
|
||||
(modal/hide))))))))]
|
||||
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[:> heading* {:level 2 :typography "headline-medium" :class (stl/css :form-modal-title)}
|
||||
(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)}
|
||||
[:> input-token*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -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")]]]]))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,105 @@
|
||||
// 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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.delete-btn {
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.token-rows {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-l);
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
}
|
||||
|
||||
.nested-input-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.typography-inputs-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-m);
|
||||
}
|
||||
|
||||
.typography-inputs {
|
||||
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;
|
||||
}
|
||||
.title {
|
||||
@include t.use-typography("body-small");
|
||||
color: var(--color-foreground-primary);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.warning-name-change-notification-wrapper {
|
||||
margin-block-start: var(--sp-l);
|
||||
}
|
||||
|
||||
.error {
|
||||
padding: var(--sp-xs) $sz-6;
|
||||
margin-block-end: 0;
|
||||
color: var(--status-color-error-500);
|
||||
}
|
||||
|
||||
.resolved-value {
|
||||
--input-hint-color: var(--color-foreground-primary);
|
||||
color: var(--input-hint-color);
|
||||
}
|
||||
|
||||
.resolved-value-error {
|
||||
--input-hint-color: var(--status-color-error-500);
|
||||
}
|
||||
|
||||
.resolved-value-warning {
|
||||
--input-hint-color: var(--status-color-warning-500);
|
||||
}
|
||||
|
||||
.form-modal-title {
|
||||
color: var(--color-foreground-primary);
|
||||
}
|
||||
|
||||
.font-select-wrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
// This padding from the modal should be shared as a variable
|
||||
// Need to set this or the font-select will cause scroll
|
||||
bottom: var(--sp-xxxl);
|
||||
}
|
||||
@@ -4,13 +4,13 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls.input
|
||||
(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 :as ds]
|
||||
[app.main.ui.ds.controls.input :refer [input*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
@@ -19,124 +19,6 @@
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; WHY WE HAVE THREE INPUT TYPES: INPUT, COMPOSITE, AND INDEXED
|
||||
;;
|
||||
;; Our token editor supports multiple token categories (colors, distances,
|
||||
;; shadows, typography, etc.). Each category stores its data inside the token’s
|
||||
;; :value field, but not all tokens have the same internal structure. To keep the
|
||||
;; UI consistent and predictable, we define three input architectures:
|
||||
;;
|
||||
;; 1) INPUTS
|
||||
;; ----------------------------------------------------------
|
||||
;; Used for tokens where the entire :value is just a single,
|
||||
;; atomic field. Examples:
|
||||
;; - :distance → a number or a reference
|
||||
;; - :text-case → one of {none, uppercase, lowercase, capitalize}
|
||||
;; - :color → a single color value or a reference
|
||||
;;
|
||||
;; Characteristics:
|
||||
;; * The input writes directly to :value.
|
||||
;; * Validation logic is simple because :value is a single unit.
|
||||
;; * Switching to "reference mode" simply replaces :value.
|
||||
;;
|
||||
;; Data shape example:
|
||||
;; {:value "16px"}
|
||||
;; {:value "{spacing.sm}"}
|
||||
;;
|
||||
;;
|
||||
;; 2) COMPOSITE INPUTS
|
||||
;; ----------------------------------------------------------
|
||||
;; Used when the token contains a set of *named fields* inside :value.
|
||||
;; The UI must write into a specific subfield inside the :value map.
|
||||
;;
|
||||
;; Example: typography tokens
|
||||
;; {:value {:font-family "Inter"
|
||||
;; :font-weight 600
|
||||
;; :letter-spacing -0.5
|
||||
;; :line-height 1.4}}
|
||||
;;
|
||||
;; Why this type exists:
|
||||
;; * Each input (font-family, weight, spacing, etc.) maps to a specific
|
||||
;; key inside :value.
|
||||
;; * The UI must update these fields individually without replacing the
|
||||
;; entire value.
|
||||
;; * Validation rules apply per-field.
|
||||
;;
|
||||
;; In practice:
|
||||
;; - The component knows which subfield to update.
|
||||
;; - The form accumulates multiple fields into a single map under :value.
|
||||
;;
|
||||
;;
|
||||
;; 3) INDEXED INPUTS
|
||||
;; ----------------------------------------------------------
|
||||
;; Used for tokens where the :value can be in TWO possible shapes:
|
||||
;; A) A direct reference to another token
|
||||
;; B) A list (array/vector) of maps describing multiple structured items
|
||||
;;
|
||||
;; Main example: shadow tokens
|
||||
;;
|
||||
;; ;; Option A — reference mode
|
||||
;; {:value {:reference "{shadow.soft}"}}
|
||||
;;
|
||||
;; ;; Option B — full definition mode
|
||||
;; {:value {:shadow
|
||||
;; [{:color "#0003"
|
||||
;; :offset-x 4
|
||||
;; :offset-y 6
|
||||
;; :blur 12
|
||||
;; :spread 0
|
||||
;; :inset? false}
|
||||
;;
|
||||
;; {:color "rgba(0,0,0,0.1)"
|
||||
;; :offset-x 0
|
||||
;; :offset-y 1
|
||||
;; :blur 3
|
||||
;; :spread 0
|
||||
;; :inset? false}]}}
|
||||
;;
|
||||
;; Why this type exists:
|
||||
;; * The UI must handle multiple items (indexed layers).
|
||||
;; * Each layer has its own internal fields (color, offsets, blur, etc.).
|
||||
;; * The user can add/remove layers dynamically.
|
||||
;; * Reference mode must disable/remove the structured mode.
|
||||
;; * Both shapes are valid, but they cannot coexist at the same time.
|
||||
;;
|
||||
;; Indexed inputs therefore need:
|
||||
;; * A tab system ("Shadow" vs "Reference")
|
||||
;; * Clear logic to ensure :shadow XOR :reference
|
||||
;; * Repetition UI (add/remove layer groups)
|
||||
;;
|
||||
;;
|
||||
;; SUMMARY
|
||||
;; -----------------------------------------------------------------------------
|
||||
;; - Plain inputs operate on :value itself.
|
||||
;; - Composite inputs operate on a map stored under :value, writing into
|
||||
;; predefined keys.
|
||||
;; - Indexed inputs operate on either:
|
||||
;; • a reference stored in :value :reference, or
|
||||
;; • an array of structured items stored in :value :shadow (or similar),
|
||||
;; but never both at the same time.
|
||||
;;
|
||||
;; This 3-tiered input system keeps the editor flexible, predictable,
|
||||
;; and compatible with the many different token types supported by the app.
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
;;
|
||||
;; Summary:
|
||||
;; -------
|
||||
;; - `input*` → single flat value tokens
|
||||
;; - `input-composite*` → structured tokens with multiple named fields
|
||||
;; (e.g. typography tokens with font-family,
|
||||
;; font-weight, letter-spacing, line-height…)
|
||||
;; - `input-indexed*` → array-based tokens where each entry is a map
|
||||
;; (e.g. multiple shadow layers)
|
||||
;;
|
||||
;; This separation ensures each form input mirrors the actual structure of the
|
||||
;; token data model, keeping validation and updates correctly scoped.
|
||||
;; -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token value]
|
||||
(let [token
|
||||
@@ -157,7 +39,7 @@
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc input*
|
||||
(mf/defc input-token*
|
||||
[{:keys [name tokens token] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
@@ -228,11 +110,11 @@
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
[:> ds/input* props]))
|
||||
[:> input* props]))
|
||||
|
||||
(defn- on-composite-input-change
|
||||
(defn- on-composite-input-token-change
|
||||
([form field value]
|
||||
(on-composite-input-change form field value false))
|
||||
(on-composite-input-token-change form field value false))
|
||||
([form field value trim?]
|
||||
(letfn [(clean-errors [errors]
|
||||
(-> errors
|
||||
@@ -244,7 +126,7 @@
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
(mf/defc input-composite*
|
||||
(mf/defc input-token-composite*
|
||||
[{:keys [name tokens token] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
@@ -273,7 +155,7 @@
|
||||
(mf/deps resolve-stream input-name)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(on-composite-input-change form input-name value true)
|
||||
(on-composite-input-token-change form input-name value true)
|
||||
(rx/push! resolve-stream value))))
|
||||
|
||||
props
|
||||
@@ -323,41 +205,43 @@
|
||||
(swap! form update :extra-errors dissoc :value)
|
||||
(if (= input-value (str value))
|
||||
(reset! hint* {})
|
||||
(reset! hint* {:message message :type "hint"})))))))]
|
||||
(reset! hint* {:message message :type "hint"})))))
|
||||
(fn [cause]
|
||||
(js/console.log "MUU" cause))))]
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
[:> ds/input* props]))
|
||||
[:> input* props]))
|
||||
|
||||
(defn- on-indexed-input-change
|
||||
([form field index value value-subfield]
|
||||
(on-indexed-input-change form field index value value-subfield false))
|
||||
([form field index value value-subfield trim?]
|
||||
(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 value-subfield index field] (if trim? (str/trim value) value))
|
||||
(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-indexed*
|
||||
[{:keys [name tokens token index value-subfield] :rest props}]
|
||||
(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 value-subfield index input-name])
|
||||
(get-in @form [:errors :value composite-type index input-name])
|
||||
|
||||
value-from-form
|
||||
(get-in @form [:data :value value-subfield index input-name] "")
|
||||
(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 value-subfield index input-name])]
|
||||
(if-let [value (get-in token [:value composite-type index input-name])]
|
||||
(rx/behavior-subject value)
|
||||
(rx/subject)))
|
||||
|
||||
@@ -372,7 +256,7 @@
|
||||
(mf/deps resolve-stream input-name index)
|
||||
(fn [event]
|
||||
(let [value (-> event dom/get-target dom/get-input-value)]
|
||||
(on-indexed-input-change form input-name index value value-subfield true)
|
||||
(on-composite-indexed-input-token-change form input-name index value composite-type true)
|
||||
(rx/push! resolve-stream value))))
|
||||
|
||||
props
|
||||
@@ -392,7 +276,7 @@
|
||||
(mf/spread-props props {:hint-formated true})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name index value-subfield]
|
||||
(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))
|
||||
@@ -406,19 +290,19 @@
|
||||
(cond
|
||||
(and error (str/empty? (:error/value error)))
|
||||
(do
|
||||
(swap! form update-in [:errors :value value-subfield index] dissoc input-name)
|
||||
(swap! form update-in [:data :value value-subfield index] dissoc input-name)
|
||||
(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 value-subfield index input-name] {: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 value-subfield index input-name] "")]
|
||||
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))
|
||||
@@ -427,4 +311,4 @@
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
[:> ds/input* props]))
|
||||
[:> input* props]))
|
||||
@@ -0,0 +1,28 @@
|
||||
;; 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-color-bullet
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.main.data.workspace.tokens.color :as dwtc]
|
||||
[app.main.ui.components.color-bullet :refer [color-bullet]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema::input-token-color-bullet
|
||||
[:map
|
||||
[:color [:maybe :string]]
|
||||
[:on-click fn?]])
|
||||
|
||||
(mf/defc input-token-color-bullet*
|
||||
{::mf/props :obj
|
||||
::mf/schema schema::input-token-color-bullet}
|
||||
[{:keys [color on-click]}]
|
||||
[:div {:data-testid "token-form-color-bullet"
|
||||
:class (stl/css :input-token-color-bullet)
|
||||
:on-click on-click}
|
||||
(if-let [color' (dwtc/color-bullet-color color)]
|
||||
[:> color-bullet {:color color' :mini true}]
|
||||
[:div {:class (stl/css :input-token-color-bullet-placeholder)}])])
|
||||
@@ -0,0 +1,28 @@
|
||||
// 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/_sizes.scss" as *;
|
||||
@use "ds/_borders.scss" as *;
|
||||
|
||||
.input-token-color-bullet {
|
||||
--bullet-size: var(--sp-l);
|
||||
--bullet-default-color: var(--color-foreground-secondary);
|
||||
--bullet-radius: var(--br-4);
|
||||
|
||||
margin-inline-end: var(--sp-s);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.input-token-color-bullet-placeholder {
|
||||
width: var(--bullet-size, --sp-l);
|
||||
height: var(--bullet-size, --sp-l);
|
||||
min-width: var(--bullet-size, --sp-l);
|
||||
min-height: var(--bullet-size, --sp-l);
|
||||
margin-top: 0;
|
||||
background-color: color-mix(in hsl, var(--bullet-default-color) 30%, transparent);
|
||||
border-radius: $br-4;
|
||||
cursor: pointer;
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
;; 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-tokens-value
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
[app.main.data.workspace.tokens.warnings :as wtw]
|
||||
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
|
||||
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
|
||||
[app.main.ui.ds.controls.utilities.label :refer [label*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon-list]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(def ^:private schema::input-token
|
||||
[:map
|
||||
[:label {:optional true} [:maybe :string]]
|
||||
[:aria-label {:optional true} [:maybe :string]]
|
||||
[:placeholder {:optional true} :string]
|
||||
[:value {:optional true} [:maybe :string]]
|
||||
[:class {:optional true} :string]
|
||||
[:error {:optional true} :boolean]
|
||||
[:slot-start {:optional true} [:maybe some?]]
|
||||
[:icon {:optional true}
|
||||
[:maybe [:and :string [:fn #(contains? icon-list %)]]]]
|
||||
[:token-resolve-result {:optional true} :any]])
|
||||
|
||||
(mf/defc token-value-hint*
|
||||
[{:keys [result]}]
|
||||
(let [{:keys [errors warnings resolved-value]} result
|
||||
empty-message? (nil? result)
|
||||
|
||||
message (cond
|
||||
empty-message? (tr "workspace.tokens.resolved-value" "-")
|
||||
warnings (->> (wtw/humanize-warnings warnings)
|
||||
(str/join "\n"))
|
||||
errors (->> (wte/humanize-errors errors)
|
||||
(str/join "\n"))
|
||||
:else (tr "workspace.tokens.resolved-value" (dwtf/format-token-value (or resolved-value result))))
|
||||
type (cond
|
||||
empty-message? "hint"
|
||||
errors "error"
|
||||
warnings "warning"
|
||||
:else "hint")]
|
||||
[:> hint-message*
|
||||
{:id "token-value-hint"
|
||||
:message message
|
||||
:class (stl/css-case :resolved-value (not (or empty-message? (seq warnings) (seq errors))))
|
||||
:type type}]))
|
||||
|
||||
(mf/defc input-token*
|
||||
{::mf/forward-ref true
|
||||
::mf/schema schema::input-token}
|
||||
[{:keys [class label token-resolve-result] :rest props} ref]
|
||||
(let [error (not (nil? (:errors token-resolve-result)))
|
||||
id (mf/use-id)
|
||||
input-ref (mf/use-ref)
|
||||
props (mf/spread-props props {:id id
|
||||
:type "text"
|
||||
:class (stl/css :input)
|
||||
:variant "comfortable"
|
||||
:hint-type (when error "error")
|
||||
:ref (or ref input-ref)})]
|
||||
[:*
|
||||
[:div {:class (dm/str class " " (stl/css-case :wrapper true
|
||||
:input-error error))}
|
||||
(when label
|
||||
[:> label* {:for id} label])
|
||||
[:> input-field* props]]
|
||||
(when token-resolve-result
|
||||
[:> token-value-hint* {:result token-resolve-result}])]))
|
||||
@@ -0,0 +1,36 @@
|
||||
// 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 *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.wrapper {
|
||||
--label-color: var(--color-foreground-primary);
|
||||
--input-bg-color: var(--color-background-tertiary);
|
||||
--input-fg-color: var(--color-foreground-primary);
|
||||
--input-icon-color: var(--color-foreground-secondary);
|
||||
--input-outline-color: none;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--sp-xs);
|
||||
|
||||
&.input-error {
|
||||
--input-outline-color: var(--color-accent-error);
|
||||
}
|
||||
}
|
||||
|
||||
.label {
|
||||
@include use-typography("body-small");
|
||||
@include deprecated.textEllipsis;
|
||||
color: var(--label-color);
|
||||
}
|
||||
|
||||
.resolved-value {
|
||||
white-space: pre;
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.modals
|
||||
(ns app.main.ui.workspace.tokens.management.create.modals
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
@@ -13,28 +13,22 @@
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.tokens.management.forms.form-container :refer [form-container*]]
|
||||
[app.main.ui.workspace.tokens.management.create.form :refer [form-wrapper*]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Component -------------------------------------------------------------------
|
||||
|
||||
(defn- calculate-position
|
||||
(defn calculate-position
|
||||
"Calculates the style properties for the given coordinates and position"
|
||||
[{vh :height} position x y token-type]
|
||||
(let [; TODO: Revisit these harcoded values
|
||||
modal-height (case token-type
|
||||
:color
|
||||
500
|
||||
:typography
|
||||
660
|
||||
:shadow
|
||||
660
|
||||
400)
|
||||
[{vh :height} position x y color?]
|
||||
(let [;; picker height in pixels
|
||||
;; TODO: Revisit these harcoded values
|
||||
h (if color? 610 510)
|
||||
;; Checks for overflow outside the viewport height
|
||||
max-y (- vh modal-height)
|
||||
overflow-fix (max 0 (+ y (- 50) modal-height (- vh)))
|
||||
max-y (- vh h)
|
||||
overflow-fix (max 0 (+ y (- 50) h (- vh)))
|
||||
bottom-offset "1rem"
|
||||
top-offset (dm/str (- y 70) "px")
|
||||
max-height-top (str "calc(100vh - " top-offset)
|
||||
@@ -67,19 +61,17 @@
|
||||
:top (dm/str (- y 70 overflow-fix) "px")
|
||||
:maxHeight max-height-top}))))
|
||||
|
||||
(defn- use-viewport-position-style [x y position token-type]
|
||||
(defn use-viewport-position-style [x y position color?]
|
||||
(let [vport (-> (l/derived :vport refs/workspace-local)
|
||||
(mf/deref))]
|
||||
(-> (calculate-position vport position x y token-type)
|
||||
(-> (calculate-position vport position x y color?)
|
||||
(clj->js))))
|
||||
|
||||
(mf/defc token-update-create-modal
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
|
||||
(let [wrapper-style (use-viewport-position-style x y position token-type)
|
||||
modal-size-large* (mf/use-state (or (= token-type :typography)
|
||||
(= token-type :color)
|
||||
(= token-type :shadow)))
|
||||
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
|
||||
modal-size-large* (mf/use-state (= token-type :typography))
|
||||
modal-size-large? (deref modal-size-large*)
|
||||
close-modal (mf/use-fn
|
||||
(fn []
|
||||
@@ -97,12 +89,12 @@
|
||||
:icon i/close
|
||||
:variant "action"
|
||||
:aria-label (tr "labels.close")}]
|
||||
[:> form-container* {:is-create (not (ctob/token? token))
|
||||
:token token
|
||||
:action action
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:token-type token-type
|
||||
:on-display-colorpicker update-modal-size}]]))
|
||||
[:> form-wrapper* {:is-create (not (ctob/token? token))
|
||||
:token token
|
||||
:action action
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:token-type token-type
|
||||
:on-display-colorpicker update-modal-size}]]))
|
||||
|
||||
;; Modals ----------------------------------------------------------------------
|
||||
|
||||
@@ -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]))
|
||||
@@ -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")]]]]))
|
||||
@@ -8,6 +8,17 @@
|
||||
@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;
|
||||
@@ -45,6 +56,29 @@
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
;; 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.text-case
|
||||
(: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.input-token :refer [input-token*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as k]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/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 {:error/fn token-value-error-fn}]]
|
||||
|
||||
[:description {:optional true}
|
||||
[:string {:max 2048 :error/fn #(tr "errors.field-max-length" 2048)}]]]
|
||||
|
||||
[:fn {:error/field :value
|
||||
:error/fn #(tr "workspace.tokens.self-reference")}
|
||||
(fn [{:keys [name value]}]
|
||||
(when (and name value)
|
||||
(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]
|
||||
(or token {:type :text-case}))
|
||||
|
||||
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)}
|
||||
[:> input-token*
|
||||
{:placeholder (tr "workspace.tokens.text-case-value-enter")
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> fc/form-input* {:id "token-description"
|
||||
:name :description
|
||||
:label (tr "workspace.tokens.token-description")
|
||||
:placeholder (tr "workspace.tokens.token-description")
|
||||
:max-length max-input-length
|
||||
:variant "comfortable"
|
||||
:is-optional true}]]
|
||||
|
||||
[:div {:class (stl/css-case :button-row true
|
||||
:with-delete (= action "edit"))}
|
||||
(when (= action "edit")
|
||||
[:> button* {:on-click on-delete-token
|
||||
:on-key-down handle-key-down-delete
|
||||
:class (stl/css :delete-btn)
|
||||
:type "button"
|
||||
:icon i/delete
|
||||
:variant "secondary"}
|
||||
(tr "labels.delete")])
|
||||
|
||||
[:> button* {:on-click on-cancel
|
||||
:on-key-down handle-key-down-cancel
|
||||
:type "button"
|
||||
:id "token-modal-cancel"
|
||||
:variant "secondary"}
|
||||
(tr "labels.cancel")]
|
||||
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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);
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
@@ -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")]]]]))
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,53 +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.forms.color
|
||||
(:require
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- token-value-error-fn
|
||||
[{:keys [value]}]
|
||||
(when (or (str/empty? value)
|
||||
(str/blank? value))
|
||||
(tr "workspace.tokens.empty-input")))
|
||||
|
||||
(defn- make-schema
|
||||
[tokens-tree _]
|
||||
(sm/schema
|
||||
[:and
|
||||
[:map
|
||||
[:name
|
||||
[:and
|
||||
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
|
||||
(sm/update-properties cto/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 {:error/fn token-value-error-fn}]]
|
||||
|
||||
[:color-result {:optional true} ::sm/any]
|
||||
|
||||
[: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*
|
||||
[props]
|
||||
(let [props (mf/spread-props props {:make-schema make-schema
|
||||
:input-component token.controls/color-input*})]
|
||||
[:> generic/form* props]))
|
||||
@@ -1,19 +0,0 @@
|
||||
(ns app.main.ui.workspace.tokens.management.forms.controls
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.color-input :as color]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.fonts-combobox :as fonts]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.input :as input]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls.select :as select]))
|
||||
|
||||
(dm/export color/color-input*)
|
||||
(dm/export color/indexed-color-input*)
|
||||
|
||||
(dm/export input/input*)
|
||||
(dm/export input/input-indexed*)
|
||||
(dm/export input/input-composite*)
|
||||
|
||||
(dm/export fonts/fonts-combobox*)
|
||||
(dm/export fonts/composite-fonts-combobox*)
|
||||
|
||||
(dm/export select/select-indexed*)
|
||||
@@ -1,45 +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.forms.controls.select
|
||||
(:require
|
||||
[app.main.ui.ds.controls.select :refer [select*]]
|
||||
[app.main.ui.forms :as fc]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Select Input (Indexed) --------------------------------------------------
|
||||
;;
|
||||
;; This input type is part of the indexed system, used for fields that exist
|
||||
;; inside an array of maps stored in a subfield of :value.
|
||||
;;
|
||||
;; - Writes to a nested location:
|
||||
;; [:value <subfield> <index> <field>]
|
||||
;; - Each item in the array has its own select input, independent of others.
|
||||
;; - Validation ensures the selected value is valid for that field.
|
||||
;; - Changing one item does not affect the other items in the array.
|
||||
;; - Ideal for properties with predefined options (boolean, enum, etc.) where
|
||||
;; multiple instances exist.
|
||||
|
||||
|
||||
(mf/defc select-indexed*
|
||||
[{:keys [name index indexed-type] :rest props}]
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
input-name name
|
||||
|
||||
value
|
||||
(get-in @form [:data :value indexed-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 indexed-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]))
|
||||
@@ -1,40 +0,0 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.main.ui.workspace.tokens.management.forms.font-family
|
||||
(:require
|
||||
[app.common.types.token :as cto]
|
||||
[app.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference default-validate-token]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- check-font-family-token-self-reference [token]
|
||||
(check-coll-self-reference (:name token) (:value token)))
|
||||
|
||||
(defn- validate-font-family-token
|
||||
[props]
|
||||
(-> props
|
||||
(update :token-value cto/split-font-family)
|
||||
(assoc :validators [(fn [token]
|
||||
(when (empty? (:value token))
|
||||
(wte/get-error-code :error.token/empty-input)))
|
||||
check-font-family-token-self-reference])
|
||||
(default-validate-token)))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token token-type] :rest props}]
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(if token
|
||||
(update token :value cto/join-font-family)
|
||||
{:type token-type}))
|
||||
props (mf/spread-props props {:token token
|
||||
:token-type token-type
|
||||
:validator validate-font-family-token
|
||||
:input-component token.controls/fonts-combobox*})]
|
||||
[:> generic/form* props]))
|
||||
@@ -1,53 +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.forms.form-container
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.management.forms.color :as color]
|
||||
[app.main.ui.workspace.tokens.management.forms.font-family :as font-family]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.main.ui.workspace.tokens.management.forms.shadow :as shadow]
|
||||
[app.main.ui.workspace.tokens.management.forms.typography :as typography]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc form-container*
|
||||
[{:keys [token token-type] :rest props}]
|
||||
(let [token-type
|
||||
(or (:type token) token-type)
|
||||
|
||||
tokens-in-selected-set
|
||||
(mf/deref refs/workspace-all-tokens-in-selected-set)
|
||||
|
||||
token-path
|
||||
(mf/with-memo [token]
|
||||
(cft/token-name->path (:name token)))
|
||||
|
||||
tokens-tree-in-selected-set
|
||||
(mf/with-memo [token-path tokens-in-selected-set]
|
||||
(-> (ctob/tokens-tree tokens-in-selected-set)
|
||||
(d/dissoc-in token-path)))
|
||||
props
|
||||
(mf/spread-props props {:token-type token-type
|
||||
:tokens-tree-in-selected-set tokens-tree-in-selected-set
|
||||
:token token})
|
||||
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
|
||||
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})
|
||||
font-weight-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.font-weight-value-enter")})]
|
||||
|
||||
(case token-type
|
||||
:color [:> color/form* props]
|
||||
:typography [:> typography/form* props]
|
||||
:shadow [:> shadow/form* props]
|
||||
:font-family [:> font-family/form* props]
|
||||
:text-case [:> generic/form* text-case-props]
|
||||
:text-decoration [:> generic/form* text-decoration-props]
|
||||
:font-weight [:> generic/form* font-weight-props]
|
||||
[:> generic/form* props])))
|
||||
@@ -1,362 +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.forms.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.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.forms :as forms]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [check-self-reference default-validate-token]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- check-shadow-token-self-reference
|
||||
"Check token when any of the attributes in a shadow's value have a self-reference."
|
||||
[token]
|
||||
(let [token-name (:name token)
|
||||
shadow-values (:value token)]
|
||||
(some (fn [[shadow-idx shadow-map]]
|
||||
(some (fn [[k v]]
|
||||
(when-let [err (check-self-reference token-name v)]
|
||||
(assoc err :shadow-key k :shadow-index shadow-idx)))
|
||||
shadow-map))
|
||||
(d/enumerate shadow-values))))
|
||||
|
||||
(defn- check-empty-shadow-token [token]
|
||||
(when (or (empty? (:value token))
|
||||
(some (fn [shadow] (not-every? #(contains? shadow %) [:offset-x :offset-y :blur :spread :color]))
|
||||
(:value token)))
|
||||
(wte/get-error-code :error.token/empty-input)))
|
||||
|
||||
(defn- validate-shadow-token
|
||||
[{: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 params)
|
||||
;; Validate composite token
|
||||
:else
|
||||
(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))))
|
||||
|
||||
(def ^:private default-token-shadow
|
||||
{:offset-x "4"
|
||||
:offset-y "4"
|
||||
:blur "4"
|
||||
:spread "0"})
|
||||
|
||||
(defn get-subtoken
|
||||
[token index prop value-subfield]
|
||||
(let [value (get-in token [:value value-subfield 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 value-subfield] :as props}]
|
||||
(let [inset-token (get-subtoken token index :inset value-subfield)
|
||||
inset-token (hooks/use-equal-memo inset-token)
|
||||
|
||||
color-token (get-subtoken token index :color value-subfield)
|
||||
color-token (hooks/use-equal-memo color-token)
|
||||
|
||||
offset-x-token (get-subtoken token index :offset-x value-subfield)
|
||||
offset-x-token (hooks/use-equal-memo offset-x-token)
|
||||
|
||||
offset-y-token (get-subtoken token index :offset-y value-subfield)
|
||||
offset-y-token (hooks/use-equal-memo offset-y-token)
|
||||
|
||||
blur-token (get-subtoken token index :blur value-subfield)
|
||||
blur-token (hooks/use-equal-memo blur-token)
|
||||
|
||||
spread-token (get-subtoken token index :spread value-subfield)
|
||||
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)}
|
||||
[:> token.controls/select-indexed* {: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
|
||||
:value-subfield value-subfield
|
||||
: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)}
|
||||
[:> token.controls/indexed-color-input*
|
||||
{:placeholder (tr "workspace.tokens.token-value-enter")
|
||||
:aria-label (tr "workspace.tokens.color")
|
||||
:name :color
|
||||
:token color-token
|
||||
:value-subfield value-subfield
|
||||
:index index
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> token.controls/input-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
|
||||
:value-subfield value-subfield
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> token.controls/input-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
|
||||
:value-subfield value-subfield
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> token.controls/input-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
|
||||
:value-subfield value-subfield
|
||||
:tokens tokens}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
[:> token.controls/input-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
|
||||
:value-subfield value-subfield
|
||||
:index index
|
||||
:tokens tokens}]]]]))
|
||||
|
||||
(mf/defc composite-form*
|
||||
[{:keys [token tokens remove-shadow-block value-subfield] :as props}]
|
||||
(let [form
|
||||
(mf/use-ctx forms/context)
|
||||
|
||||
length
|
||||
(-> form deref :data :value value-subfield count)]
|
||||
|
||||
(for [index (range length)]
|
||||
[:> shadow-formset* {:key index
|
||||
:index index
|
||||
:token token
|
||||
:tokens tokens
|
||||
:value-subfield value-subfield
|
||||
: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)}
|
||||
[:> token.controls/input-composite*
|
||||
{:placeholder (tr "workspace.tokens.reference-composite-shadow")
|
||||
:aria-label (tr "labels.reference")
|
||||
:icon i/drop-shadow
|
||||
:name :reference
|
||||
:token token
|
||||
:tokens tokens}]])
|
||||
|
||||
(mf/defc tabs-wrapper*
|
||||
[{:keys [token tokens form tab toggle subfield] :rest props}]
|
||||
(let [on-add-shadow-block
|
||||
(mf/use-fn
|
||||
(mf/deps subfield)
|
||||
(fn []
|
||||
(swap! form update-in [:data :value subfield] conj default-token-shadow)))
|
||||
|
||||
remove-shadow-block
|
||||
(mf/use-fn
|
||||
(mf/deps subfield)
|
||||
(fn [index event]
|
||||
(dom/prevent-default event)
|
||||
(swap! form update-in [:data :value subfield] #(d/remove-at-index % index))))]
|
||||
|
||||
[:*
|
||||
[: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 tab)
|
||||
:on-change toggle
|
||||
: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 (= tab :composite)
|
||||
[:> composite-form* {:token token
|
||||
:tokens tokens
|
||||
:remove-shadow-block remove-shadow-block
|
||||
:value-subfield subfield}]
|
||||
|
||||
[:> reference-form* {: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?)))]]))
|
||||
|
||||
(defn- make-default-value
|
||||
[value]
|
||||
(cond
|
||||
(string? value)
|
||||
{:reference value
|
||||
:shadow []}
|
||||
|
||||
(vector? value)
|
||||
{:reference nil
|
||||
:shadow value}
|
||||
|
||||
:else
|
||||
{:reference nil
|
||||
:shadow [default-token-shadow]}))
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token
|
||||
token-type] :as props}]
|
||||
(let [token
|
||||
(mf/with-memo [token]
|
||||
(or token
|
||||
(if-let [value (get token :value)]
|
||||
{:type token-type
|
||||
:value (make-default-value value)}
|
||||
|
||||
{:type token-type
|
||||
:value {:reference nil
|
||||
:shadow [default-token-shadow]}})))
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
(let [raw-value (:value token)
|
||||
value (make-default-value raw-value)]
|
||||
|
||||
{:name (:name token "")
|
||||
:description (:description token "")
|
||||
:value value}))
|
||||
|
||||
props (mf/spread-props props {:token token
|
||||
:token-type token-type
|
||||
:initial initial
|
||||
:make-schema make-schema
|
||||
:type :indexed
|
||||
:value-subfield :shadow
|
||||
:input-component tabs-wrapper*
|
||||
:validator validate-shadow-token})]
|
||||
[:> generic/form* props]))
|
||||
@@ -1,304 +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.forms.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.main.data.workspace.tokens.errors :as wte]
|
||||
[app.main.ui.components.radio-buttons :refer [radio-button radio-buttons]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[app.main.ui.workspace.tokens.management.forms.validators :refer [check-coll-self-reference check-self-reference default-validate-token]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; VALIDATORS
|
||||
|
||||
(defn- check-typography-token-self-reference
|
||||
"Check token when any of the attributes in token value have a self-reference."
|
||||
[token]
|
||||
(let [token-name (:name token)
|
||||
token-values (:value token)]
|
||||
(some (fn [[k v]]
|
||||
(when-let [err (case k
|
||||
:font-family (check-coll-self-reference token-name v)
|
||||
(check-self-reference token-name v))]
|
||||
(assoc err :typography-key k)))
|
||||
token-values)))
|
||||
|
||||
(defn- check-empty-typography-token [token]
|
||||
(when (empty? (:value token))
|
||||
(wte/get-error-code :error.token/empty-input)))
|
||||
|
||||
(defn- validate-typography-token
|
||||
[{:keys [token-value] :as props}]
|
||||
(cond
|
||||
;; Entering form without a value - show no error just resolve nil
|
||||
(nil? token-value) (rx/of nil)
|
||||
;; Validate refrence string
|
||||
(cto/typography-composite-token-reference? token-value) (default-validate-token props)
|
||||
;; Validate composite token
|
||||
:else
|
||||
(-> props
|
||||
(update :token-value
|
||||
(fn [v]
|
||||
(-> (or v {})
|
||||
(d/update-when :font-family #(if (string? %) (cto/split-font-family %) %)))))
|
||||
(assoc :validators [check-empty-typography-token
|
||||
check-typography-token-self-reference])
|
||||
(default-validate-token))))
|
||||
|
||||
;; COMPONENTS
|
||||
|
||||
(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}))
|
||||
|
||||
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)}
|
||||
[:> token.controls/composite-fonts-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-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)}
|
||||
[:> token.controls/input-composite*
|
||||
{:placeholder (tr "workspace.tokens.reference-composite")
|
||||
:aria-label (tr "labels.reference")
|
||||
:icon i/text-typography
|
||||
:name :reference
|
||||
:token token
|
||||
:tokens tokens}]])
|
||||
|
||||
(mf/defc tabs-wrapper*
|
||||
[{:keys [token tokens tab toggle] :rest props}]
|
||||
[:*
|
||||
[:div {:class (stl/css :title-bar)}
|
||||
[:div {:class (stl/css :title)} (tr "labels.typography")]
|
||||
[:& radio-buttons {:class (stl/css :listing-options)
|
||||
:selected (d/name tab)
|
||||
:on-change toggle
|
||||
: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 (= tab :composite)
|
||||
[:> composite-form* {:token token
|
||||
:tokens tokens}]
|
||||
|
||||
[:> reference-form* {:token token
|
||||
:tokens tokens}])]])
|
||||
|
||||
;; SCHEMA
|
||||
|
||||
(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] :as props}]
|
||||
(let [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 "")}))
|
||||
props (mf/spread-props props {:initial initial
|
||||
:make-schema make-schema
|
||||
:token token
|
||||
:validator validate-typography-token
|
||||
:type :composite
|
||||
:input-component tabs-wrapper*})]
|
||||
[:> generic/form* props]))
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user