Compare commits

..

5 Commits

Author SHA1 Message Date
Andrey Antukh
4545047eda ♻️ Make several improvements to management API authentication 2026-01-26 14:56:51 +01:00
David Barragán Merino
c5f03d711a 🔧 Enable secret inheritance 2026-01-26 14:00:09 +01:00
David Barragán Merino
72cc5ee349 🔧 Define deploy plugin packages workflows 2026-01-26 13:23:46 +01:00
Eva Marco
804695b48b ♻️ Replace stroke width numeric inputs (#8137)
*  Replace opacity numeric input

*  Add test

* ♻️ Replace stroke width numeric input

* 🎉 Add tests
2026-01-26 12:50:28 +01:00
Elena Torró
20c8fbf314 🔧 Make render-wasm visual regression tests use gpu (#8189) 2026-01-26 11:53:29 +01:00
23 changed files with 883 additions and 85 deletions

View File

@@ -1,11 +1,127 @@
name: Plugins/package deployer
on:
# Deploy package from manual action
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
plugin_name:
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
type: string
required: true
workflow_call:
inputs:
gh_ref:
description: 'Name of the branch'
type: string
required: true
default: 'develop'
plugin_name:
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
type: string
required: true
permissions:
contents: read
jobs:
print_text_job:
runs-on: ubuntu-latest
deploy:
runs-on: penpot-runner-01
steps:
- name: Print Hello World
run: echo "Hello, World!"
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ inputs.gh_ref }}
# START: Setup Node and PNPM enabling cache
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: .nvmrc
- name: Enable PNPM
working-directory: ./plugins
shell: bash
run: |
corepack enable;
corepack install;
- name: Get pnpm store path
id: pnpm-store
working-directory: ./plugins
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-
# END: Setup Node and PNPM enabling cache
- name: Install deps
working-directory: ./plugins
shell: bash
run: |
pnpm install --no-frozen-lockfile;
pnpm add -D -w wrangler@latest;
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
working-directory: plugins
shell: bash
run: npx nx build ${{ inputs.plugin_name }}-plugin
- name: Select Worker name
run: |
REF="${{ inputs.gh_ref }}"
case "$REF" in
main)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
staging)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
develop)
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
*) echo "Unsupported branch ${REF}" && exit 1 ;;
esac
- name: Set the custom url
working-directory: plugins
shell: bash
run: |
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
- name: Deploy to Cloudflare Workers
uses: cloudflare/wrangler-action@v3
with:
workingDirectory: plugins
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
- name: Notify Mattermost
if: failure()
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
📄 Triggered from ref: `${{ inputs.gh_ref }}`
Plugin name: `${{ inputs.plugin_name }}-plugin`
Cloudflare worker name: `${{ env.WORKER_NAME }}`
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra

View File

@@ -1,11 +1,143 @@
name: Plugins/packages deployer
on:
push:
branches:
- develop
- staging
- main
paths:
- 'plugins/apps/*-plugin/**'
- 'libs/plugins-styles/**'
workflow_dispatch:
inputs:
gh_ref:
description: 'Name of the branch'
type: choice
required: true
default: 'develop'
options:
- develop
- staging
- main
jobs:
print_text_job:
detect-changes:
runs-on: ubuntu-latest
outputs:
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
create_palette: ${{ steps.filter.outputs.create_palette }}
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
rename_layers: ${{ steps.filter.outputs.rename_layers }}
contrast: ${{ steps.filter.outputs.contrast }}
icons: ${{ steps.filter.outputs.icons }}
poc_state: ${{ steps.filter.outputs.poc_state }}
table: ${{ steps.filter.outputs.table }}
# [For new plugins]
# Add more outputs here
steps:
- name: Print Hello World
run: echo "Hello, World!"
- uses: actions/checkout@v4
- id: filter
uses: dorny/paths-filter@v3
with:
filters: |
colors_to_tokens:
- 'plugins/apps/colors-to-tokens-plugin/**'
- 'libs/plugins-styles/**'
contrast:
- 'plugins/apps/contrast-plugin/**'
- 'libs/plugins-styles/**'
create_palette:
- 'plugins/apps/create-palette-plugin/**'
- 'libs/plugins-styles/**'
icons:
- 'plugins/apps/icons-plugin/**'
- 'libs/plugins-styles/**'
lorem_ipsum:
- 'plugins/apps/lorem-ipsum-plugin/**'
- 'libs/plugins-styles/**'
rename_layers:
- 'plugins/apps/rename-layers-plugin/**'
- 'libs/plugins-styles/**'
table:
- 'plugins/apps/table-plugin/**'
- 'libs/plugins-styles/**'
# [For new plugins]
# Add more plugin filters here
# another_plugin:
# - 'plugins/apps/another-plugin/**'
# - 'libs/plugins-styles/**'
colors-to-tokens-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.colors_to_tokens == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: colors-to-tokens
contrast-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.contrast == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: contrast
create-palette-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.create_palette == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: create-palette
icons-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.icons == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: icons
lorem-ipsum-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.lorem_ipsum == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: lorem-ipsum
rename-layers-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.rename_layers == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: rename-layers
table-plugin:
needs: detect-changes
if: needs.detect-changes.outputs.table == 'true'
uses: ./.github/workflows/plugins-deploy-package.yml
secrets: inherit
with:
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
plugin_name: table
# [For new plugins]
# Add more jobs for other plugins below, following the same pattern
# another-plugin:
# needs: detect-changes
# if: needs.detect-changes.outputs.another_plugin == 'true'
# uses: ./.github/workflows/plugins-deploy-package.yml
# secrets: inherit
# with:
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
# plugin_name: another

View File

@@ -30,7 +30,6 @@
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
## 2.13.0 (Unreleased)

View File

@@ -1,7 +1,12 @@
#!/usr/bin/env bash
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
export PENPOT_HOST=devenv
export PENPOT_PUBLIC_URI=https://localhost:3449

View File

@@ -102,6 +102,8 @@
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]

View File

@@ -13,13 +13,13 @@
[app.common.time :as ct]
[app.config :as cf]
[app.db :as db]
[app.http.middleware :as mw]
[app.main :as-alias main]
[app.rpc.commands.profile :as cmd.profile]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[app.worker :as-alias wrk]
[integrant.core :as ig]
[yetti.request :as yreq]
[yetti.response :as-alias yres]))
;; ---- ROUTES
@@ -49,28 +49,40 @@
(fn [cfg request]
(db/tx-run! cfg handler request)))))})
(def ^:private shared-key-auth
{:name ::shared-key-auth
:compile
(fn [_ _]
(fn [handler key]
(if key
(fn [request]
(if-let [key' (yreq/get-header request "x-shared-key")]
(if (= key key')
(handler request)
{::yres/status 403})
{::yres/status 403}))
(fn [_ _]
{::yres/status 403}))))})
(defmethod ig/init-key ::routes
[_ {:keys [::setup/props] :as cfg}]
[_ cfg]
(let [management-key (or (cf/get :management-api-key)
(get props :management-key))]
["" {:middleware [[shared-key-auth (cf/get :management-api-key)]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["" {:middleware [[mw/shared-key-auth management-key]
[default-system cfg]
[transaction]]}
["/authenticate"
{:handler authenticate
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/get-customer"
{:handler get-customer
:transaction true
:allowed-methods #{:post}}]
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]]))
["/update-customer"
{:handler update-customer
:allowed-methods #{:post}
:transaction true}]])
;; ---- HELPERS

View File

@@ -16,7 +16,6 @@
[app.http.errors :as errors]
[app.tokens :as tokens]
[app.util.pointer-map :as pmap]
[buddy.core.codecs :as bc]
[cuerdas.core :as str]
[yetti.adapter :as yt]
[yetti.middleware :as ymw]
@@ -301,16 +300,20 @@
:compile (constantly wrap-auth)})
(defn- wrap-shared-key-auth
[handler shared-key]
(if shared-key
(let [shared-key (if (string? shared-key)
shared-key
(bc/bytes->b64-str shared-key true))]
(fn [request]
(let [key (yreq/get-header request "x-shared-key")]
(if (= key shared-key)
(handler (assoc request ::http/auth-with-shared-key true))
{::yres/status 403}))))
[handler keys]
(if (seq keys)
(fn [request]
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
(str/split #"\s+" 2))]
(let [key-id (str/lower key-id)]
(if (and (string? key)
(contains? keys key-id)
(= key (get keys key-id)))
(-> request
(assoc ::http/auth-key-id key-id)
(handler))
{::yres/status 403}))
{::yres/status 403}))
(fn [_ _]
{::yres/status 403})))

View File

@@ -140,10 +140,14 @@
client-version (get-client-version request)
client-user-agent (get-client-user-agent request)
session-id (get-external-session-id request)
token-id (::actoken/id request)]
key-id (::http/auth-key-id request)
token-id (::actoken/id request)
token-type (::actoken/type request)]
(d/without-nils
{:external-session-id session-id
:initiator (or key-id "app")
:access-token-id (some-> token-id str)
:access-token-type (some-> token-type str)
:client-event-origin client-event-origin
:client-user-agent client-user-agent
:client-version client-version

View File

@@ -275,8 +275,7 @@
::email/whitelist (ig/ref ::email/whitelist)}
::mgmt/routes
{::db/pool (ig/ref ::db/pool)
::setup/props (ig/ref ::setup/props)}
{::db/pool (ig/ref ::db/pool)}
:app.http/router
{::session/manager (ig/ref ::session/manager)
@@ -352,13 +351,13 @@
::setup/props (ig/ref ::setup/props)}
::rpc/routes
{::rpc/methods (ig/ref :app.rpc/methods)
{::rpc/methods (ig/ref :app.rpc/methods)
::rpc/management-methods (ig/ref :app.rpc/management-methods)
;; FIXME: revisit if db/pool is necessary here
::db/pool (ig/ref ::db/pool)
::session/manager (ig/ref ::session/manager)
::setup/props (ig/ref ::setup/props)}
::setup/shared-keys (ig/ref ::setup/shared-keys)}
::wrk/registry
{::mtx/metrics (ig/ref ::mtx/metrics)
@@ -446,6 +445,11 @@
;; module requires the migrations to run before initialize.
::migrations (ig/ref :app.migrations/migrations)}
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock
{}

View File

@@ -92,11 +92,11 @@
(fn [{:keys [params path-params method] :as request}]
(let [handler-name (:type path-params)
etag (yreq/get-header request "if-none-match")
key-id (get request ::http/auth-key-id)
profile-id (or (::session/profile-id request)
(::actoken/profile-id request)
(if (::http/auth-with-shared-key request)
uuid/zero
nil))
(if key-id uuid/zero nil))
ip-addr (inet/parse-request request)
@@ -345,23 +345,20 @@
(defmethod ig/assert-key ::routes
[_ params]
(assert (map? (::setup/shared-keys params)))
(assert (db/pool? (::db/pool params)) "expect valid database pool")
(assert (some? (::setup/props params)))
(assert (session/manager? (::session/manager params)) "expect valid session manager")
(assert (valid-methods? (::methods params)) "expect valid methods map")
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
(defmethod ig/init-key ::routes
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
(let [public-uri (cf/get :public-uri)
management-key (or (cf/get :management-api-key)
(get props :management-key))]
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}]
(let [public-uri (cf/get :public-uri)]
["/api"
["/management"
["/methods/:type"
{:middleware [[mw/shared-key-auth management-key]
{:middleware [[mw/shared-key-auth shared-keys]
[session/authz cfg]]
:handler (make-rpc-handler management-methods)}]

View File

@@ -17,6 +17,7 @@
[app.setup.templates]
[buddy.core.codecs :as bc]
[buddy.core.nonce :as bn]
[cuerdas.core :as str]
[integrant.core :as ig]))
(defn- generate-random-key
@@ -88,7 +89,38 @@
(-> (get-all-props conn)
(assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens"))
(assoc :management-key (keys/derive secret :salt "management"))
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
(sm/register! ::props [:map-of :keyword ::sm/any])
(defmethod ig/init-key ::shared-keys
[_ {:keys [::props] :as cfg}]
(let [secret (get props :secret-key)]
(d/without-nils
{"exporter"
(let [key (or (get cfg :exporter)
(-> (keys/derive secret :salt "exporter")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "exporter key is disabled because empty string found")
nil)
(do
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key)))
"nitrate"
(let [key (or (get cfg :nitrate)
(-> (keys/derive secret :salt "nitrate")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "nitrate key is disabled because empty string found")
nil)
(do
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
key)))})))

View File

@@ -86,7 +86,7 @@
(t/deftest shared-key-auth
(let [handler (#'app.http.middleware/wrap-shared-key-auth
(fn [req] {::yres/status 200})
"secret-key")]
{"test1" "secret-key"})]
(let [response (handler (->DummyRequest {} {}))]
(t/is (= 403 (::yres/status response))))
@@ -95,6 +95,9 @@
(t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
(t/is (= 403 (::yres/status response))))
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
(t/is (= 200 (::yres/status response))))))
(t/deftest access-token-authz

View File

@@ -488,6 +488,7 @@
:sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number}
:opacity #{:opacity}
:stroke-width #{:stroke-width :dimensions}
:font-size #{:font-size}
:letter-spacing #{:letter-spacing}
:fill #{:color}

View File

@@ -12,11 +12,14 @@
["node:process" :as process]
[app.common.data :as d]
[app.common.flags :as flags]
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.version :as v]
[cljs.core :as c]
[cuerdas.core :as str]))
(l/set-level! :info)
(def ^:private defaults
{:public-uri "http://localhost:3449"
:tenant "default"
@@ -30,7 +33,7 @@
[:map {:title "config"}
[:secret-key :string]
[:public-uri {:optional true} ::sm/uri]
[:management-api-key {:optional true} :string]
[:exporter-shared-key {:optional true} :string]
[:host {:optional true} :string]
[:tenant {:optional true} :string]
[:flags {:optional true} [::sm/set :keyword]]
@@ -98,8 +101,10 @@
(c/get config key default)))
(def management-key
(or (c/get config :management-api-key)
(let [secret-key (c/get config :secret-key)
derived-key (crypto/hkdfSync "blake2b512" secret-key, "management" "" 32)]
(-> (.from buffer/Buffer derived-key)
(.toString "base64url")))))
(let [key (or (c/get config :exporter-shared-key)
(let [secret-key (c/get config :secret-key)
derived-key (crypto/hkdfSync "blake2b512" secret-key, "exporter" "" 32)]
(-> (.from buffer/Buffer derived-key)
(.toString "base64url"))))]
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key))

View File

@@ -73,7 +73,7 @@
(p/mcat (fn [blob]
(let [fdata (new http/FormData)
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
headers #js {"X-Shared-Key" cf/management-key
headers #js {"X-Shared-Key" (str "exporter " cf/management-key)
"Authorization" (str "Bearer " auth-token)}
request #js {:headers headers

View File

@@ -64,6 +64,11 @@ export default defineConfig({
...devices["Desktop Chrome"],
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
deviceScaleFactor: 2,
launchOptions: {
args: [
'--enable-gpu',
],
}
},
testDir: "./playwright/ui/render-wasm-specs",
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",

View File

@@ -0,0 +1,352 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u55ffbd0f-2d3f-8023-8007-6b89af7ca1aa",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Nuevo Archivo 2",
"~:revn": 18,
"~:modified-at": "~m1768994999265",
"~:vern": 0,
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314bfe8",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~u55ffbd0f-2d3f-8023-8007-6b89af7cd5eb",
"~:created-at": "~m1768562403410",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u8bb92298-e24e-805c-8007-6ce64314cfc5"
],
"~:pages-index": {
"~u8bb92298-e24e-805c-8007-6ce64314cfc5": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~u8506e3f3-e05b-807c-8007-6ceac380abc1\"]]]",
"~u6c65a5dc-fb40-8072-8007-6ce644905054": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:layout-align-items\",\"~:start\",\"~:width\",389,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",303,\"~:y\",159]],[\"^L\",[\"^ \",\"~:x\",692,\"~:y\",159]],[\"^L\",[\"^ \",\"~:x\",692,\"~:y\",599]],[\"^L\",[\"^ \",\"~:x\",303,\"~:y\",599]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",303,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",303,\"~:y\",159,\"^F\",389,\"~:height\",440,\"~:x1\",303,\"~:y1\",159,\"~:x2\",692,\"~:y2\",599]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^16\",440,\"~:flip-y\",null,\"~:shapes\",[\"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9\",\"~u6c65a5dc-fb40-8072-8007-6ce65472f217\",\"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62\"]]]",
"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",95,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",303,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",259]],[\"^<\",[\"^ \",\"~:x\",303,\"~:y\",259]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",303,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",303,\"~:y\",159,\"^8\",95,\"~:height\",100,\"~:x1\",303,\"~:y1\",159,\"~:x2\",398,\"~:y2\",259]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",100,\"~:flip-y\",null]]",
"~u6c65a5dc-fb40-8072-8007-6ce65472f217": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",140.99999999999994,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",398,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",296]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",296]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce65472f217\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",398.00000000000006,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",398.00000000000006,\"~:y\",159,\"^8\",140.99999999999994,\"~:height\",137,\"~:x1\",398.00000000000006,\"~:y1\",159,\"~:x2\",539,\"~:y2\",296]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]",
"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",82,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",539,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",621,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",621,\"~:y\",259]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",259]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",539,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",539,\"~:y\",159,\"^8\",82,\"~:height\",100,\"~:x1\",539,\"~:y1\",159,\"~:x2\",621,\"~:y2\",259]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",100,\"~:flip-y\",null]]",
"~u8506e3f3-e05b-807c-8007-6ceac380abc1": "[\"~#shape\",[\"^ \",\"~:y\",405,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",59,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817,\"~:y\",405]],[\"^<\",[\"^ \",\"~:x\",876,\"~:y\",405]],[\"^<\",[\"^ \",\"~:x\",876,\"~:y\",472]],[\"^<\",[\"^ \",\"~:x\",817,\"~:y\",472]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u8506e3f3-e05b-807c-8007-6ceac380abc1\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817,\"~:y\",405,\"^8\",59,\"~:height\",67,\"~:x1\",817,\"~:y1\",405,\"~:x2\",876,\"~:y2\",472]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",67,\"~:flip-y\",null]]"
}
},
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314cfc5",
"~:name": "Page 1"
}
},
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314bfe8",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
},
"~:tokens-lib": {
"~#penpot/tokens-lib": {
"~:sets": {
"~#ordered-map": [
[
"S-Global",
{
"~#penpot/token-set": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce67f8f2592",
"~:name": "Global",
"~:description": "",
"~:modified-at": "~m1768994999268",
"~:tokens": {
"~#ordered-map": [
[
"dim.xs",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce67f8f2591",
"~:name": "dim.xs",
"~:type": "~:dimensions",
"~:value": "20",
"~:description": "",
"~:modified-at": "~m1768562465340"
}
}
],
[
"dim.md",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce68a2f3c3f",
"~:name": "dim.md",
"~:type": "~:dimensions",
"~:value": "50",
"~:description": "",
"~:modified-at": "~m1768562476220"
}
}
],
[
"dim.xl",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce694f47726",
"~:name": "dim.xl",
"~:type": "~:dimensions",
"~:value": "100",
"~:description": "",
"~:modified-at": "~m1768562487249"
}
}
],
[
"sz.sm",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6d6e519b1",
"~:name": "sz.sm",
"~:type": "~:sizing",
"~:value": "200",
"~:description": "",
"~:modified-at": "~m1768562554772"
}
}
],
[
"sz.xl",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6e4165307",
"~:name": "sz.xl",
"~:type": "~:sizing",
"~:value": "500",
"~:description": "",
"~:modified-at": "~m1768562568281"
}
}
],
[
"sp.mid",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6f5fb2867",
"~:name": "sp.mid",
"~:type": "~:spacing",
"~:value": "50",
"~:description": "",
"~:modified-at": "~m1768562586604"
}
}
],
[
"sp.l",
{
"~#penpot/token": {
"~:id": "~u7c1a66f7-5186-8060-8007-6ce71009cd91",
"~:name": "sp.l",
"~:type": "~:spacing",
"~:value": "{dim.xl}",
"~:description": "",
"~:modified-at": "~m1768562613287"
}
}
],
[
"test",
{
"~#penpot/token": {
"~:id": "~uc49f25e6-1298-8004-8007-709f660875be",
"~:name": "test",
"~:type": "~:dimensions",
"~:value": "20",
"~:description": "",
"~:modified-at": "~m1768812262433"
}
}
],
[
"width-big",
{
"~#penpot/token": {
"~:id": "~u3b3e4320-53fb-8096-8007-73585edb4dd6",
"~:name": "width-big",
"~:type": "~:stroke-width",
"~:value": "20",
"~:description": "",
"~:modified-at": "~m1768994969453"
}
}
],
[
"width-small",
{
"~#penpot/token": {
"~:id": "~u3b3e4320-53fb-8096-8007-73586a5ec516",
"~:name": "width-small",
"~:type": "~:stroke-width",
"~:value": "5",
"~:description": "",
"~:modified-at": "~m1768994981243"
}
}
],
[
"red",
{
"~#penpot/token": {
"~:id": "~u3b3e4320-53fb-8096-8007-735871dccede",
"~:name": "red",
"~:type": "~:color",
"~:value": "red",
"~:description": "",
"~:modified-at": "~m1768994988915"
}
}
],
[
"green",
{
"~#penpot/token": {
"~:id": "~u3b3e4320-53fb-8096-8007-735879045aa2",
"~:name": "green",
"~:type": "~:color",
"~:value": "green",
"~:description": "",
"~:modified-at": "~m1768994996241"
}
}
]
]
}
}
}
]
]
},
"~:themes": {
"~#ordered-map": [
[
"",
{
"~#ordered-map": [
[
"__PENPOT__HIDDEN__TOKEN__THEME__",
{
"~#penpot/token-theme": {
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:name": "__PENPOT__HIDDEN__TOKEN__THEME__",
"~:group": "",
"~:description": "",
"~:is-source": false,
"~:external-id": "",
"~:modified-at": "~m1768562468391",
"~:sets": {
"~#set": [
"Global"
]
}
}
}
]
]
}
]
]
},
"~:active-themes": {
"~#set": [
"/__PENPOT__HIDDEN__TOKEN__THEME__"
]
}
}
}
}
}

View File

@@ -681,7 +681,8 @@ test.describe("Tokens: Apply token", () => {
await dimensionXSTokenPill.click();
// Change token from dropdown
const dimensionTokenOptionXl = borderRadiusSection.getByLabel("dimension.xl");
const dimensionTokenOptionXl =
borderRadiusSection.getByLabel("dimension.xl");
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
@@ -698,5 +699,64 @@ test.describe("Tokens: Apply token", () => {
await detachButton.nth(0).click();
await expect(dimensionXLTokenPill).not.toBeVisible();
});
test("User applies stroke width token to a shape", async ({ page }) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
// Set up
await workspace.mockConfigFlags(["enable-feature-token-input"]);
await workspace.setupEmptyFile();
await workspace.mockGetFile("workspace/get-file-layout-stroke-token-json");
await workspace.goToWorkspace();
// Select shape apply stroke
await workspace.layers.getByTestId("layer-row").nth(0).click();
const rightSidebar = page.getByTestId("right-sidebar");
await expect(rightSidebar).toBeVisible();
await rightSidebar.getByTestId("add-stroke").click();
// Apply stroke width token from token panel
const tokensTab = page.getByRole("tab", { name: "Tokens" });
await expect(tokensTab).toBeVisible();
await tokensTab.click();
await page.getByRole("button", { name: "Stroke Width 2" }).click();
const tokensSidebar = workspace.tokensSidebar;
await expect(
tokensSidebar.getByRole("button", { name: "width-big" }),
).toBeVisible();
await tokensSidebar.getByRole("button", { name: "width-big" }).click();
// Check if token pill is visible on right sidebar
const strokeSectionSidebar = rightSidebar.getByRole("region", {
name: "stroke-section",
});
await expect(strokeSectionSidebar).toBeVisible();
const firstStrokeRow = strokeSectionSidebar.getByLabel("stroke-row-0");
await expect(firstStrokeRow).toBeVisible();
const StrokeWidthPill = firstStrokeRow.getByRole("button", {
name: "width-big",
});
await expect(StrokeWidthPill).toBeVisible();
// Detach token from right sidebar and apply another from dropdown
const detachButton = firstStrokeRow.getByRole("button", {
name: "Detach token",
});
await detachButton.click();
await expect(StrokeWidthPill).not.toBeVisible();
const tokenDropdown = firstStrokeRow.getByRole("button", {
name: "Open token list",
});
await tokenDropdown.click();
const widthOptionSmall = firstStrokeRow.getByLabel("width-small");
await expect(widthOptionSmall).toBeVisible();
await widthOptionSmall.click();
const StrokeWidthPillSmall = firstStrokeRow.getByRole("button", {
name: "width-small",
});
await expect(StrokeWidthPillSmall).toBeVisible();
});
});

View File

@@ -534,10 +534,10 @@
(set (filter attributes #{:r1 :r2 :r3 :r4}))
page-id)))
(some attributes #{:strole-width})
(some attributes #{:stroke-width})
(conj #(update-stroke-width
value shape-ids
#{:strole-width}
#{:stroke-width}
page-id))
(some attributes #{:max-width :max-height})
(conj #(update-layout-sizing-limits

View File

@@ -437,8 +437,7 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.flatten")
:on-click do-transform-to-path}])
(when (and (not has-frame?)
(not disable-booleans)
(when (and (not disable-booleans)
(or multiple? (and single? (or is-group? is-bool?))))
[:> menu-entry* {:title (tr "workspace.shape.menu.path")}
[:> menu-entry* {:title (tr "workspace.shape.menu.union")

View File

@@ -176,7 +176,8 @@
:token token
:shape-ids ids}))))]
[:div {:class (stl/css :stroke-section)}
[:section {:class (stl/css :stroke-section)
:aria-label "stroke-section"}
[:div {:class (stl/css :stroke-title)}
[:> title-bar* {:collapsable has-strokes?
:collapsed (not open?)

View File

@@ -9,18 +9,50 @@
(:require
[app.common.data :as d]
[app.common.types.color :as ctc]
[app.common.types.token :as tk]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.features :as features]
[app.main.store :as st]
[app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.numeric-input :as deprecated-input]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.context :as muc]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc numeric-input-wrapper*
{::mf/private true}
[{:keys [values name applied-tokens align on-detach] :rest props}]
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
tokens (mf/with-memo [tokens name]
(delay
(-> (deref tokens)
(select-keys (get tk/tokens-by-input name))
(not-empty))))
on-detach-attr (mf/use-fn
(mf/deps on-detach name)
#(on-detach % name))
applied-token (get applied-tokens name)
props (mf/spread-props props
{:placeholder (if (= :multiple values)
(tr "settings.multiple")
"--")
:applied-token applied-token
:tokens (if (delay? tokens) @tokens tokens)
:align align
:on-detach on-detach-attr
:name name
:value values})]
[:> numeric-input* props]))
(mf/defc stroke-row*
[{:keys [index
stroke
@@ -45,7 +77,10 @@
select-on-focus
ids]}]
(let [on-drop
(let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
on-drop
(mf/use-fn
(mf/deps on-reorder index)
(fn [relative-pos data]
@@ -88,7 +123,13 @@
on-width-change
(mf/use-fn
(mf/deps index on-stroke-width-change)
#(on-stroke-width-change index %))
(fn [value]
(if (or (string? value) (int? value))
(on-stroke-width-change index value)
(do
(st/emit! (dwta/toggle-token {:token (first value)
:attrs #{:stroke-width}
:shape-ids ids}))))))
stroke-alignment (or (:stroke-alignment stroke) :center)
@@ -149,6 +190,12 @@
(fn [token]
(on-detach-token token #{:stroke-color})))
on-detach-token-width
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(on-detach-token (first token) #{:stroke-width})))
stroke-caps-options
[{:value nil :label (tr "workspace.options.stroke-cap.none")}
:separator
@@ -169,7 +216,8 @@
[:div {:class (stl/css-case
:stroke-data true
:dnd-over-top (= (:over dprops) :top)
:dnd-over-bot (= (:over dprops) :bot))}
:dnd-over-bot (= (:over dprops) :bot))
:aria-label (str "stroke-row-" index)}
(when (some? on-reorder)
[:> reorder-handler* {:ref dref}])
@@ -195,17 +243,30 @@
;; Stroke Width, Alignment & Style
[:div {:class (stl/css :stroke-options)}
[:div {:class (stl/css :stroke-width-input)
:title (tr "workspace.options.stroke-width")}
[:> icon* {:icon-id i/stroke-size
:size "s"}]
[:> numeric-input* {:value stroke-width
:min 0
:placeholder (tr "settings.multiple")
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
:on-blur on-blur}]]
(if token-numeric-inputs
[:> numeric-input-wrapper* {:on-change on-width-change
:on-detach on-detach-token-width
:icon i/stroke-size
:min 0
:on-focus on-focus
:on-blur on-blur
:name :stroke-width
:class (stl/css :numeric-input-wrapper)
:property (tr "workspace.options.stroke-width")
:applied-tokens applied-tokens
:values stroke-width}]
[:div {:class (stl/css :stroke-width-input)
:title (tr "workspace.options.stroke-width")}
[:> icon* {:icon-id i/stroke-size
:size "s"}]
[:> deprecated-input/numeric-input* {:value stroke-width
:min 0
:placeholder (tr "settings.multiple")
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
:on-blur on-blur}]])
[:div {:class (stl/css :stroke-alignment-select)
:data-testid "stroke.alignment"}

View File

@@ -45,6 +45,11 @@
padding-inline-start: var(--sp-xs);
}
.numeric-input-wrapper {
grid-column: span 2;
--dropdown-width: var(--7-columns-dropdown-width);
}
.stroke-alignment-select {
grid-column: span 3;
}