mirror of
https://github.com/penpot/penpot.git
synced 2026-02-13 08:01:39 -05:00
Compare commits
6 Commits
eva-create
...
niwinz-plu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e2f355ce07 | ||
|
|
483ead59fe | ||
|
|
d8d532ed4f | ||
|
|
a7cec4573d | ||
|
|
6049fa1c96 | ||
|
|
a0fef67c16 |
28
SECURITY.md
28
SECURITY.md
@@ -2,30 +2,4 @@
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We take the security of this project seriously. If you have discovered
|
||||
a security vulnerability, please do **not** open a public issue.
|
||||
|
||||
Please report vulnerabilities via email to: **[support@penpot.app]**
|
||||
|
||||
|
||||
### What to include:
|
||||
|
||||
* A brief description of the vulnerability.
|
||||
* Steps to reproduce the issue.
|
||||
* Potential impact if exploited.
|
||||
|
||||
We appreciate your patience and your commitment to **responsible disclosure**.
|
||||
|
||||
---
|
||||
|
||||
## Security Contributors
|
||||
|
||||
We are incredibly grateful to the following individuals and
|
||||
organizations for their help in keeping this project safe.
|
||||
|
||||
* **Ali Maharramli** – for identifying critical path traversal vulnerability
|
||||
|
||||
|
||||
> **Note:** This list is a work in progress. If you have contributed
|
||||
> to the security of this project and would like to be recognized (or
|
||||
> prefer to remain anonymous), please let us know.
|
||||
Please report security issues to `support@penpot.app`
|
||||
@@ -126,6 +126,12 @@ http {
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
location /plugins {
|
||||
autoindex on;
|
||||
alias /home/penpot/penpot/plugins/dist/apps;
|
||||
proxy_http_version 1.1;
|
||||
}
|
||||
|
||||
location /mcp/ws {
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
@@ -198,6 +198,13 @@ services:
|
||||
## Valkey (or previously Redis) is used for the websockets notifications.
|
||||
PENPOT_REDIS_URI: redis://penpot-valkey/0
|
||||
|
||||
penpot-mcp:
|
||||
image: penpotapp/mcp:${PENPOT_VERSION:-latest}
|
||||
restart: always
|
||||
|
||||
networks:
|
||||
- penpot
|
||||
|
||||
penpot-postgres:
|
||||
image: "postgres:15"
|
||||
restart: always
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
"license": "MPL-2.0",
|
||||
"author": "Kaleidos INC",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
|
||||
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||
"browserslist": [
|
||||
"defaults"
|
||||
],
|
||||
@@ -42,12 +42,13 @@
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"watch": "exit 0",
|
||||
"watch:app": "pnpm run clear:shadow-cache && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"",
|
||||
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
|
||||
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\"",
|
||||
"postinstall": "(cd ../plugins/libs/plugins-runtime; pnpm run build)"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@penpot/draft-js": "workspace:./packages/draft-js",
|
||||
"@penpot/mousetrap": "workspace:./packages/mousetrap",
|
||||
"@penpot/plugins-runtime": "1.4.2",
|
||||
"@penpot/plugins-runtime": "link:../plugins/libs/plugins-runtime",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/text-editor": "workspace:./text-editor",
|
||||
"@penpot/tokenscript": "workspace:./packages/tokenscript",
|
||||
|
||||
47
frontend/pnpm-lock.yaml
generated
47
frontend/pnpm-lock.yaml
generated
@@ -20,8 +20,8 @@ importers:
|
||||
specifier: workspace:./packages/mousetrap
|
||||
version: link:packages/mousetrap
|
||||
'@penpot/plugins-runtime':
|
||||
specifier: 1.4.2
|
||||
version: 1.4.2
|
||||
specifier: link:../plugins/libs/plugins-runtime
|
||||
version: link:../plugins/libs/plugins-runtime
|
||||
'@penpot/svgo':
|
||||
specifier: penpot/svgo#v3.2
|
||||
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
|
||||
@@ -581,15 +581,6 @@ packages:
|
||||
'@dabh/diagnostics@2.0.8':
|
||||
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==}
|
||||
|
||||
'@endo/cache-map@1.1.0':
|
||||
resolution: {integrity: sha512-owFGshs/97PDw9oguZqU/px8Lv1d0KjAUtDUiPwKHNXRVUE/jyettEbRoTbNJR1OaI8biMn6bHr9kVJsOh6dXw==}
|
||||
|
||||
'@endo/env-options@1.1.11':
|
||||
resolution: {integrity: sha512-p9OnAPsdqoX4YJsE98e3NBVhIr2iW9gNZxHhAI2/Ul5TdRfoOViItzHzTqrgUVopw6XxA1u1uS6CykLMDUxarA==}
|
||||
|
||||
'@endo/immutable-arraybuffer@1.1.2':
|
||||
resolution: {integrity: sha512-u+NaYB2aqEugQ3u7w3c5QNkPogf8q/xGgsPaqdY6pUiGWtYiTiFspKFcha6+oeZhWXWQ23rf0KrUq0kfuzqYyQ==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
|
||||
engines: {node: '>=12'}
|
||||
@@ -1258,12 +1249,6 @@ packages:
|
||||
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
|
||||
'@penpot/plugin-types@1.4.2':
|
||||
resolution: {integrity: sha512-O8wU6RSYE8bIVU7g8cSTYi32ppxs3R13dq7X3Nn9tmDaJjBOKOBpVLuoRPIp3fJC65fv8/7om0sdrtFoL5v19g==}
|
||||
|
||||
'@penpot/plugins-runtime@1.4.2':
|
||||
resolution: {integrity: sha512-y9TDZOnb96JBW9E33dHKpmTMeAPXLtHDIZruUVjtM8hBJWZK7RCv+vAGDGxeoZJC/OB2YAHrCZG+mukePBzcuQ==}
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
|
||||
engines: {node: '>=14'}
|
||||
@@ -4636,9 +4621,6 @@ packages:
|
||||
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
|
||||
engines: {node: '>= 18'}
|
||||
|
||||
ses@1.14.0:
|
||||
resolution: {integrity: sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==}
|
||||
|
||||
set-function-length@1.2.2:
|
||||
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5499,9 +5481,6 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.25.0 || ^4.0.0
|
||||
|
||||
zod@3.25.76:
|
||||
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||
|
||||
zod@4.3.6:
|
||||
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
|
||||
|
||||
@@ -5775,12 +5754,6 @@ snapshots:
|
||||
enabled: 2.0.0
|
||||
kuler: 2.0.0
|
||||
|
||||
'@endo/cache-map@1.1.0': {}
|
||||
|
||||
'@endo/env-options@1.1.11': {}
|
||||
|
||||
'@endo/immutable-arraybuffer@1.1.2': {}
|
||||
|
||||
'@esbuild/aix-ppc64@0.21.5':
|
||||
optional: true
|
||||
|
||||
@@ -6297,14 +6270,6 @@ snapshots:
|
||||
'@parcel/watcher-win32-x64': 2.5.6
|
||||
optional: true
|
||||
|
||||
'@penpot/plugin-types@1.4.2': {}
|
||||
|
||||
'@penpot/plugins-runtime@1.4.2':
|
||||
dependencies:
|
||||
'@penpot/plugin-types': 1.4.2
|
||||
ses: 1.14.0
|
||||
zod: 3.25.76
|
||||
|
||||
'@pkgjs/parseargs@0.11.0':
|
||||
optional: true
|
||||
|
||||
@@ -10000,12 +9965,6 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ses@1.14.0:
|
||||
dependencies:
|
||||
'@endo/cache-map': 1.1.0
|
||||
'@endo/env-options': 1.1.11
|
||||
'@endo/immutable-arraybuffer': 1.1.2
|
||||
|
||||
set-function-length@1.2.2:
|
||||
dependencies:
|
||||
define-data-property: 1.1.4
|
||||
@@ -10974,6 +10933,4 @@ snapshots:
|
||||
dependencies:
|
||||
zod: 4.3.6
|
||||
|
||||
zod@3.25.76: {}
|
||||
|
||||
zod@4.3.6: {}
|
||||
|
||||
@@ -66,22 +66,22 @@
|
||||
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
|
||||
|
||||
(defn- load-plugin!
|
||||
[{:keys [plugin-id name description host code icon permissions]}]
|
||||
[{:keys [plugin-id name version description host code icon permissions]}]
|
||||
(try
|
||||
(st/emit! (save-current-plugin plugin-id)
|
||||
(reset-plugin-flags plugin-id))
|
||||
|
||||
(.ɵloadPlugin
|
||||
^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:description description
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
(.ɵloadPlugin ^js ug/global
|
||||
#js {:pluginId plugin-id
|
||||
:name name
|
||||
:description description
|
||||
:version version
|
||||
:host host
|
||||
:code code
|
||||
:icon icon
|
||||
:permissions (apply array permissions)}
|
||||
(fn []
|
||||
(st/emit! (remove-current-plugin plugin-id))))
|
||||
|
||||
(catch :default e
|
||||
(st/emit! (remove-current-plugin plugin-id))
|
||||
|
||||
@@ -35,7 +35,6 @@
|
||||
(def ^:private schema:options-dropdown
|
||||
[:map
|
||||
[:ref {:optional true} fn?]
|
||||
[:wrapper-ref {:optional true} :any]
|
||||
[:on-click fn?]
|
||||
[:options [:vector schema:option]]
|
||||
[:selected {:optional true} :any]
|
||||
@@ -84,7 +83,6 @@
|
||||
:name name
|
||||
:resolved (get option :resolved-value)
|
||||
:ref ref
|
||||
:role "option"
|
||||
:focused (= id focused)
|
||||
:on-click on-click}]
|
||||
|
||||
@@ -96,7 +94,6 @@
|
||||
:aria-label (get option :aria-label)
|
||||
:icon (get option :icon)
|
||||
:ref ref
|
||||
:role "option"
|
||||
:focused (= id focused)
|
||||
:dimmed (true? (:dimmed option))
|
||||
:on-click on-click}]))))
|
||||
@@ -104,7 +101,7 @@
|
||||
|
||||
(mf/defc options-dropdown*
|
||||
{::mf/schema schema:options-dropdown}
|
||||
[{:keys [ref on-click options selected focused empty-to-end align wrapper-ref] :rest props}]
|
||||
[{:keys [ref on-click options selected focused empty-to-end align] :rest props}]
|
||||
(let [align
|
||||
(d/nilv align :left)
|
||||
|
||||
@@ -113,7 +110,6 @@
|
||||
{:class (stl/css-case :option-list true
|
||||
:left-align (= align :left)
|
||||
:right-align (= align :right))
|
||||
:ref wrapper-ref
|
||||
:tab-index "-1"
|
||||
:role "listbox"})
|
||||
|
||||
|
||||
@@ -1,95 +0,0 @@
|
||||
(ns app.main.ui.ds.controls.utilities.utils
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.token :as cto]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]))
|
||||
|
||||
(defn- token->dropdown-option
|
||||
[token]
|
||||
{:id (str (get token :id))
|
||||
:type :token
|
||||
:resolved-value (get token :value)
|
||||
:name (get token :name)})
|
||||
|
||||
(defn- generate-dropdown-options
|
||||
[tokens no-sets]
|
||||
(if (empty? tokens)
|
||||
[{:type :empty
|
||||
:label (if no-sets
|
||||
(tr "ds.inputs.numeric-input.no-applicable-tokens")
|
||||
(tr "ds.inputs.numeric-input.no-matches"))}]
|
||||
(->> tokens
|
||||
(map (fn [[type items]]
|
||||
(cons {:group true
|
||||
:type :group
|
||||
:id (dm/str "group-" (name type))
|
||||
:name (name type)}
|
||||
(map token->dropdown-option items))))
|
||||
(interpose [{:separator true
|
||||
:id "separator"
|
||||
:type :separator}])
|
||||
(apply concat)
|
||||
(vec)
|
||||
(not-empty))))
|
||||
|
||||
(defn- extract-partial-brace-text
|
||||
[s]
|
||||
(when-let [start (str/last-index-of s "{")]
|
||||
(subs s (inc start))))
|
||||
|
||||
(defn- filter-token-groups-by-name
|
||||
[tokens filter-text]
|
||||
(let [lc-filter (str/lower filter-text)]
|
||||
(into {}
|
||||
(keep (fn [[group tokens]]
|
||||
(let [filtered (filter #(str/includes? (str/lower (:name %)) lc-filter) tokens)]
|
||||
(when (seq filtered)
|
||||
[group filtered]))))
|
||||
tokens)))
|
||||
|
||||
(defn- sort-groups-and-tokens
|
||||
"Sorts both the groups and the tokens inside them alphabetically.
|
||||
|
||||
Input:
|
||||
A map where:
|
||||
- keys are groups (keywords or strings, e.g. :dimensions, :colors)
|
||||
- values are vectors of token maps, each containing at least a :name key
|
||||
|
||||
Example input:
|
||||
{:dimensions [{:name \"tres\"} {:name \"quini\"}]
|
||||
:colors [{:name \"azul\"} {:name \"rojo\"}]}
|
||||
|
||||
Output:
|
||||
A sorted map where:
|
||||
- groups are ordered alphabetically by key
|
||||
- tokens inside each group are sorted alphabetically by :name
|
||||
|
||||
Example output:
|
||||
{:colors [{:name \"azul\"} {:name \"rojo\"}]
|
||||
:dimensions [{:name \"quini\"} {:name \"tres\"}]}"
|
||||
|
||||
[groups->tokens]
|
||||
(into (sorted-map) ;; ensure groups are ordered alphabetically by their key
|
||||
(for [[group tokens] groups->tokens]
|
||||
[group (sort-by :name tokens)])))
|
||||
|
||||
(defn get-token-dropdown-options
|
||||
[tokens filter-term]
|
||||
(delay
|
||||
(let [tokens (if (delay? tokens) @tokens tokens)
|
||||
|
||||
sorted-tokens (sort-groups-and-tokens tokens)
|
||||
partial (extract-partial-brace-text filter-term)
|
||||
options (if (seq partial)
|
||||
(filter-token-groups-by-name sorted-tokens partial)
|
||||
sorted-tokens)
|
||||
no-sets? (nil? sorted-tokens)]
|
||||
(generate-dropdown-options options no-sets?))))
|
||||
|
||||
(defn filter-tokens-for-input
|
||||
[raw-tokens input-type]
|
||||
(delay
|
||||
(-> (deref raw-tokens)
|
||||
(select-keys (get cto/tokens-by-input input-type))
|
||||
(not-empty))))
|
||||
@@ -143,7 +143,8 @@
|
||||
(let [token-ids (set tokens-in-path-ids)
|
||||
remaining-tokens (filter (fn [token]
|
||||
(not (contains? token-ids (:id token))))
|
||||
selected-token-set-tokens)]
|
||||
selected-token-set-tokens)
|
||||
_ (prn "Remaining tokens:" remaining-tokens)]
|
||||
(seq remaining-tokens))))
|
||||
|
||||
delete-token
|
||||
|
||||
@@ -1,20 +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.border-radius
|
||||
(:require
|
||||
[app.main.ui.workspace.tokens.management.forms.controls :as token.controls]
|
||||
[app.main.ui.workspace.tokens.management.forms.generic-form :as generic]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
|
||||
(mf/defc form*
|
||||
[{:keys [token token-type] :rest props}]
|
||||
(let [props (mf/spread-props props {:token token
|
||||
:token-type token-type
|
||||
:input-component token.controls/combobox*})]
|
||||
[:> generic/form* props]))
|
||||
@@ -2,7 +2,6 @@
|
||||
(: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.combobox :as combobox]
|
||||
[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]))
|
||||
@@ -17,6 +16,4 @@
|
||||
(dm/export fonts/fonts-combobox*)
|
||||
(dm/export fonts/composite-fonts-combobox*)
|
||||
|
||||
(dm/export select/select-indexed*)
|
||||
|
||||
(dm/export combobox/combobox*)
|
||||
(dm/export select/select-indexed*)
|
||||
@@ -1,410 +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.combobox
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.input :as ds]
|
||||
[app.main.ui.ds.controls.select :refer [get-option]]
|
||||
[app.main.ui.ds.controls.shared.options-dropdown :refer [options-dropdown*]]
|
||||
[app.main.ui.ds.controls.utilities.utils :as csu]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.forms :as fc]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.forms :as fm]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- focusable-option?
|
||||
[option]
|
||||
(and (:id option)
|
||||
(not= :group (:type option))
|
||||
(not= :separator (:type option))))
|
||||
|
||||
(defn- first-focusable-id
|
||||
[options]
|
||||
(some #(when (focusable-option? %) (:id %)) options))
|
||||
|
||||
(defn next-focus-id
|
||||
[options focused-id direction]
|
||||
(let [focusable (filter focusable-option? options)
|
||||
ids (map :id focusable)
|
||||
idx (.indexOf (clj->js ids) focused-id)
|
||||
next-idx (case direction
|
||||
:down (min (dec (count ids)) (inc (if (= idx -1) -1 idx)))
|
||||
:up (max 0 (dec (if (= idx -1) 0 idx))))]
|
||||
(nth ids next-idx nil)))
|
||||
|
||||
|
||||
(defn extract-partial-token
|
||||
[value cursor]
|
||||
(let [text-before (subs value 0 cursor)
|
||||
last-open (str/last-index-of text-before "{")
|
||||
last-close (str/last-index-of text-before "}")]
|
||||
(when (and last-open (or (nil? last-close) (> last-open last-close)))
|
||||
{:start last-open
|
||||
:partial (subs text-before (inc last-open))})))
|
||||
|
||||
(defn replace-active-token
|
||||
[value cursor new-name]
|
||||
|
||||
(let [before (subs value 0 cursor)
|
||||
last-open (str/last-index-of before "{")
|
||||
last-close (str/last-index-of before "}")]
|
||||
|
||||
(if (and last-open
|
||||
(or (nil? last-close)
|
||||
(> last-open last-close)))
|
||||
|
||||
(let [after-start (subs value last-open)
|
||||
close-pos (str/index-of after-start "}")
|
||||
end (if close-pos
|
||||
(+ last-open close-pos 1)
|
||||
cursor)]
|
||||
(str (subs value 0 last-open)
|
||||
"{" new-name "}"
|
||||
(subs value end)))
|
||||
(str (subs value 0 cursor)
|
||||
"{" new-name "}"
|
||||
(subs value cursor)))))
|
||||
|
||||
(defn active-token [value input-node]
|
||||
(let [cursor (.-selectionStart input-node)]
|
||||
(extract-partial-token value cursor)))
|
||||
|
||||
(defn remove-self-token [filtered-options current-token]
|
||||
(let [group (:type current-token)
|
||||
current-id (:id current-token)
|
||||
filtered-options (deref filtered-options)]
|
||||
(update filtered-options group
|
||||
(fn [options]
|
||||
(remove #(= (:id %) current-id) options)))))
|
||||
|
||||
|
||||
(defn- select-option-by-id
|
||||
[id options-ref input-node value]
|
||||
(let [cursor (.-selectionStart input-node)
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)
|
||||
|
||||
option (get-option options id)
|
||||
name (:name option)
|
||||
final-val (replace-active-token value cursor name)]
|
||||
final-val))
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(re-matches cto/token-name-validation-regex token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
tokens
|
||||
(-> tokens
|
||||
;; Remove previous token when renaming a token
|
||||
(dissoc (:name prev-token))
|
||||
(update (:name token) #(ctob/make-token (merge % prev-token token))))]
|
||||
|
||||
(->> tokens
|
||||
(sd/resolve-tokens-interactive)
|
||||
(rx/mapcat
|
||||
(fn [resolved-tokens]
|
||||
(let [{:keys [errors resolved-value] :as resolved-token} (get resolved-tokens (:name token))]
|
||||
(if resolved-value
|
||||
(rx/of {:value resolved-value})
|
||||
(rx/of {:error (first errors)}))))))))
|
||||
|
||||
(mf/defc combobox*
|
||||
[{:keys [name tokens token token-type empty-to-end ref] :rest props}]
|
||||
|
||||
(let [form (mf/use-ctx fc/context)
|
||||
_ (prn "token" token)
|
||||
|
||||
input-name name
|
||||
token-name (get-in @form [:data :name] nil)
|
||||
|
||||
is-open* (mf/use-state false)
|
||||
is-open (deref is-open*)
|
||||
|
||||
listbox-id (mf/use-id)
|
||||
filter-term* (mf/use-state "")
|
||||
filter-term (deref filter-term*)
|
||||
|
||||
focused-id* (mf/use-state nil)
|
||||
focused-id (deref focused-id*)
|
||||
|
||||
options-ref (mf/use-ref nil)
|
||||
dropdown-ref (mf/use-ref nil)
|
||||
internal-ref (mf/use-ref nil)
|
||||
nodes-ref (mf/use-ref nil)
|
||||
icon-button-ref (mf/use-ref nil)
|
||||
ref (or ref internal-ref)
|
||||
|
||||
touched?
|
||||
(and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
|
||||
error
|
||||
(get-in @form [:errors input-name])
|
||||
|
||||
value
|
||||
(get-in @form [:data input-name] "")
|
||||
|
||||
raw-tokens-by-type (mf/use-ctx muc/active-tokens-by-type)
|
||||
|
||||
filtered-tokens-by-type
|
||||
(mf/with-memo [raw-tokens-by-type token-type]
|
||||
(csu/filter-tokens-for-input raw-tokens-by-type token-type))
|
||||
|
||||
visible-options
|
||||
(mf/with-memo [filtered-tokens-by-type token]
|
||||
(if token
|
||||
(remove-self-token filtered-tokens-by-type token)
|
||||
filtered-tokens-by-type))
|
||||
|
||||
dropdown-options
|
||||
(mf/with-memo [visible-options filter-term]
|
||||
(csu/get-token-dropdown-options visible-options (str "{" filter-term)))
|
||||
|
||||
set-option-ref
|
||||
(mf/use-fn
|
||||
(fn [node]
|
||||
(let [state (mf/ref-val nodes-ref)
|
||||
state (d/nilv state #js {})
|
||||
id (dom/get-data node "id")
|
||||
state (obj/set! state id node)]
|
||||
(mf/set-ref-val! nodes-ref state))))
|
||||
|
||||
toggle-dropdown
|
||||
(mf/use-fn
|
||||
(mf/deps)
|
||||
(fn [event]
|
||||
(dom/prevent-default event)
|
||||
(swap! is-open* not)))
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if (contains? token :value)
|
||||
(rx/behavior-subject (:value token))
|
||||
(rx/subject)))
|
||||
|
||||
on-change
|
||||
(mf/use-fn
|
||||
(mf/deps resolve-stream input-name form)
|
||||
(fn [event]
|
||||
(let [node (dom/get-target event)
|
||||
value (dom/get-input-value node)
|
||||
token (active-token value node)]
|
||||
|
||||
(fm/on-input-change form input-name value)
|
||||
(rx/push! resolve-stream value)
|
||||
|
||||
(if token
|
||||
(do
|
||||
(reset! is-open* true)
|
||||
(reset! filter-term* (:partial token)))
|
||||
(do
|
||||
(reset! is-open* false)
|
||||
(reset! filter-term* ""))))))
|
||||
|
||||
on-option-click
|
||||
(mf/use-fn
|
||||
(mf/deps value resolve-stream ref)
|
||||
(fn [event]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
node (dom/get-current-target event)
|
||||
id (dom/get-data node "id")
|
||||
final-val (select-option-by-id id options-ref input-node value)]
|
||||
|
||||
(fm/on-input-change form input-name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false)
|
||||
|
||||
(dom/focus! input-node)
|
||||
(let [new-cursor (+ (str/index-of final-val "}") 1)]
|
||||
(set! (.-selectionStart input-node) new-cursor)
|
||||
(set! (.-selectionEnd input-node) new-cursor)))))
|
||||
|
||||
on-option-enter
|
||||
(mf/use-fn
|
||||
(mf/deps focused-id value resolve-stream)
|
||||
(fn [_]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
final-val (select-option-by-id focused-id options-ref input-node value)]
|
||||
(fm/on-input-change form input-name final-val true)
|
||||
(rx/push! resolve-stream final-val)
|
||||
(reset! filter-term* "")
|
||||
(reset! is-open* false))))
|
||||
|
||||
on-key-down
|
||||
(mf/use-fn
|
||||
(mf/deps is-open focused-id)
|
||||
(fn [event]
|
||||
(let [up? (kbd/up-arrow? event)
|
||||
down? (kbd/down-arrow? event)
|
||||
enter? (kbd/enter? event)
|
||||
esc? (kbd/esc? event)
|
||||
open-dropdown (kbd/is-key? event "{")
|
||||
close-dropdown (kbd/is-key? event "}")
|
||||
options (mf/ref-val options-ref)
|
||||
options (if (delay? options) @options options)]
|
||||
|
||||
(cond
|
||||
open-dropdown
|
||||
(reset! is-open* true)
|
||||
|
||||
close-dropdown
|
||||
(reset! is-open* false)
|
||||
|
||||
down?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(let [next-id (next-focus-id options focused-id :down)]
|
||||
(reset! focused-id* next-id))
|
||||
(when (some? @filtered-tokens-by-type)
|
||||
(do
|
||||
(toggle-dropdown event)
|
||||
(reset! focused-id* (first-focusable-id options))))))
|
||||
|
||||
up?
|
||||
(when is-open
|
||||
(dom/prevent-default event)
|
||||
(let [next-id (next-focus-id options focused-id :up)]
|
||||
(reset! focused-id* next-id)))
|
||||
|
||||
enter?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(if is-open
|
||||
(on-option-enter event)
|
||||
(do
|
||||
(reset! focused-id* (first-focusable-id options))
|
||||
(toggle-dropdown event))))
|
||||
|
||||
esc?
|
||||
(do
|
||||
(dom/prevent-default event)
|
||||
(reset! is-open* false))))))
|
||||
|
||||
handle-blur
|
||||
(fn [event]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
dropdown-node (mf/ref-val dropdown-ref)
|
||||
next-focus (.-relatedTarget event)]
|
||||
|
||||
(when (and next-focus
|
||||
(not (dom/child? next-focus input-node))
|
||||
(not (dom/child? next-focus dropdown-node)))
|
||||
(reset! is-open* false))))
|
||||
|
||||
hint*
|
||||
(mf/use-state {})
|
||||
|
||||
hint
|
||||
(deref hint*)
|
||||
|
||||
props
|
||||
(mf/spread-props props {:on-change on-change
|
||||
:value value
|
||||
:variant "comfortable"
|
||||
:hint-message (:message hint)
|
||||
:on-key-down on-key-down
|
||||
:hint-type (:type hint)
|
||||
:ref ref
|
||||
:role "combobox"
|
||||
:aria-activedescendant focused-id
|
||||
:aria-controls listbox-id
|
||||
:aria-expanded is-open
|
||||
:slot-end
|
||||
(when (some? @filtered-tokens-by-type)
|
||||
(mf/html
|
||||
[:> icon-button*
|
||||
{:variant "action"
|
||||
:icon i/arrow-down
|
||||
:ref icon-button-ref
|
||||
:tooltip-class (stl/css :button-tooltip)
|
||||
:class (stl/css :invisible-button)
|
||||
:tab-index "-1"
|
||||
:aria-label (tr "ds.inputs.numeric-input.open-token-list-dropdown")
|
||||
:on-click toggle-dropdown}]))})
|
||||
props
|
||||
(if (and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name token-name]
|
||||
(let [subs (->> resolve-stream
|
||||
(rx/debounce 300)
|
||||
(rx/mapcat (partial resolve-value tokens token token-name))
|
||||
(rx/map (fn [result]
|
||||
(d/update-when result :error
|
||||
(fn [error]
|
||||
((:error/fn error) (:error/value error))))))
|
||||
(rx/subs! (fn [{:keys [error value]}]
|
||||
(let [touched? (get-in @form [:touched input-name])]
|
||||
(when touched?
|
||||
(if error
|
||||
(do
|
||||
(swap! form assoc-in [:extra-errors input-name] {:message error})
|
||||
(reset! hint* {:message error :type "error"}))
|
||||
(let [message (tr "workspace.tokens.resolved-value" value)]
|
||||
(swap! form update :extra-errors dissoc input-name)
|
||||
(reset! hint* {:message message :type "hint"}))))))))]
|
||||
(fn []
|
||||
(rx/dispose! subs))))
|
||||
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
(mf/with-effect [is-open* ref nodes-ref]
|
||||
(when @is-open*
|
||||
(let [handler (fn [event]
|
||||
(let [input-node (mf/ref-val ref)
|
||||
dropdown-node (mf/ref-val dropdown-ref)
|
||||
target (dom/get-target event)]
|
||||
(when (and input-node dropdown-node
|
||||
(not (dom/child? target input-node))
|
||||
(not (dom/child? target dropdown-node)))
|
||||
(reset! is-open* false))))]
|
||||
|
||||
(.addEventListener js/document "mousedown" handler)
|
||||
|
||||
(fn []
|
||||
(.removeEventListener js/document "mousedown" handler)))))
|
||||
|
||||
[:div {:class (stl/css :combobox-wrapper)
|
||||
:on-blur handle-blur}
|
||||
[:> ds/input* props]
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:focused focused-id
|
||||
:selected nil
|
||||
:align :right
|
||||
:style {:top "56px"}
|
||||
:empty-to-end empty-to-end
|
||||
:wrapper-ref dropdown-ref
|
||||
:ref set-option-ref}]))]))
|
||||
@@ -1,3 +0,0 @@
|
||||
.combobox-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -9,7 +9,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.ui.workspace.tokens.management.forms.border-radius :as test]
|
||||
[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]
|
||||
@@ -50,5 +49,4 @@
|
||||
:text-case [:> generic/form* text-case-props]
|
||||
:text-decoration [:> generic/form* text-decoration-props]
|
||||
:font-weight [:> generic/form* font-weight-props]
|
||||
:border-radius [:> test/form* props]
|
||||
[:> generic/form* props])))
|
||||
@@ -21,7 +21,6 @@
|
||||
[app.main.data.workspace.tokens.remapping :as remap]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.context :as muc]
|
||||
[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*]]
|
||||
@@ -98,10 +97,6 @@
|
||||
(and (:name token) (:value token))
|
||||
(assoc (:name token) token)))
|
||||
|
||||
active-tokens-by-type
|
||||
(mf/with-memo [tokens]
|
||||
(delay (ctob/group-by-type tokens)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||
(make-schema tokens-tree-in-selected-set active-tab))
|
||||
@@ -229,80 +224,78 @@
|
||||
error-message (first error-messages)]
|
||||
(swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))]
|
||||
|
||||
[(mf/provider muc/active-tokens-by-type) {:value active-tokens-by-type}
|
||||
[:> fc/form* {:class (stl/css :form-wrapper)
|
||||
:form form
|
||||
:on-submit on-submit}
|
||||
[:div {:class (stl/css :token-rows)}
|
||||
[:> 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))]
|
||||
[:> 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"
|
||||
:trim true
|
||||
:auto-focus true}]]
|
||||
[: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"
|
||||
:trim true
|
||||
:auto-focus true}]]
|
||||
|
||||
[:div {:class (stl/css :input-row)}
|
||||
(case type
|
||||
:indexed
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:value-subfield value-subfield
|
||||
:handle-toggle on-toggle-tab}]
|
||||
[:div {:class (stl/css :input-row)}
|
||||
(case type
|
||||
:indexed
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:value-subfield value-subfield
|
||||
:handle-toggle on-toggle-tab}]
|
||||
|
||||
:composite
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:handle-toggle on-toggle-tab}]
|
||||
:composite
|
||||
[:> input-component
|
||||
{:token token
|
||||
:tokens tokens
|
||||
:tab active-tab
|
||||
:handle-toggle on-toggle-tab}]
|
||||
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-placeholder
|
||||
(tr "workspace.tokens.token-value-enter"))
|
||||
:label (tr "workspace.tokens.token-value")
|
||||
:name :value
|
||||
:token token
|
||||
:token-type token-type
|
||||
:tokens tokens}])]
|
||||
[:> input-component
|
||||
{:placeholder (or input-value-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 :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")])
|
||||
[: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")]
|
||||
[:> 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")]]]]]))
|
||||
[:> fc/form-submit* {:variant "primary"
|
||||
:on-submit on-submit}
|
||||
(tr "labels.save")]]]]))
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
(d/without-nils
|
||||
{:plugin-id plugin-id
|
||||
:url (str plugin-url)
|
||||
:version vers
|
||||
:name name
|
||||
:description desc
|
||||
:host origin
|
||||
|
||||
@@ -12,7 +12,10 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/contrast-plugin",
|
||||
"outputPath": {
|
||||
"base": "dist/apps/contrast-plugin",
|
||||
"browser": "",
|
||||
},
|
||||
"index": "apps/contrast-plugin/src/index.html",
|
||||
"browser": "apps/contrast-plugin/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
@@ -20,6 +23,7 @@
|
||||
"assets": [
|
||||
"apps/contrast-plugin/src/_headers",
|
||||
"apps/contrast-plugin/src/favicon.ico",
|
||||
"apps/contrast-plugin/src/manifest.json",
|
||||
"apps/contrast-plugin/src/assets"
|
||||
],
|
||||
"styles": [
|
||||
@@ -218,7 +222,10 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/table-plugin",
|
||||
"outputPath": {
|
||||
"base": "dist/apps/table-plugin",
|
||||
"browser": ""
|
||||
},
|
||||
"index": "apps/table-plugin/src/index.html",
|
||||
"browser": "apps/table-plugin/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
@@ -226,6 +233,7 @@
|
||||
"assets": [
|
||||
"apps/table-plugin/src/_headers",
|
||||
"apps/table-plugin/src/favicon.ico",
|
||||
"apps/table-plugin/src/manifest.json",
|
||||
"apps/table-plugin/src/assets"
|
||||
],
|
||||
"styles": [
|
||||
@@ -356,7 +364,10 @@
|
||||
"build": {
|
||||
"builder": "@angular-devkit/build-angular:application",
|
||||
"options": {
|
||||
"outputPath": "dist/apps/colors-to-tokens-plugin",
|
||||
"outputPath": {
|
||||
"base": "dist/apps/colors-to-tokens-plugin",
|
||||
"browser": ""
|
||||
},
|
||||
"index": "apps/colors-to-tokens-plugin/src/index.html",
|
||||
"browser": "apps/colors-to-tokens-plugin/src/main.ts",
|
||||
"polyfills": ["zone.js"],
|
||||
@@ -364,6 +375,7 @@
|
||||
"assets": [
|
||||
"apps/colors-to-tokens-plugin/src/_headers",
|
||||
"apps/colors-to-tokens-plugin/src/favicon.ico",
|
||||
"apps/colors-to-tokens-plugin/src/manifest.json",
|
||||
"apps/colors-to-tokens-plugin/src/assets"
|
||||
],
|
||||
"styles": [
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "ng build colors-to-tokens-plugin",
|
||||
"build:dev": "ng build colors-to-tokens-plugin --configuration development",
|
||||
"watch": "ng build colors-to-tokens-plugin --configuration development --watch",
|
||||
"serve": "ng serve colors-to-tokens-plugin",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest"
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, withHashLocation } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter([])],
|
||||
};
|
||||
providers: [
|
||||
provideRouter([], withHashLocation())
|
||||
],
|
||||
};
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>colors-to-tokens-plugin</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "Colors to Tokens",
|
||||
"description": "Generate a design tokens file from a list of colors",
|
||||
"code": "/assets/plugin.js",
|
||||
"icon": "/assets/icon.png",
|
||||
"version": 2,
|
||||
"code": "assets/plugin.js",
|
||||
"icon": "assets/icon.png",
|
||||
"permissions": ["content:read", "library:read", "allow:downloads"]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, withHashLocation } from '@angular/router';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter([])],
|
||||
providers: [provideRouter([], withHashLocation())],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>contrast-plugin</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "Contrast",
|
||||
"description": "Measure contrast plugin",
|
||||
"code": "/assets/plugin.js",
|
||||
"icon": "/assets/icon.png",
|
||||
"version": 2,
|
||||
"code": "assets/plugin.js",
|
||||
"icon": "assets/icon.png",
|
||||
"permissions": ["content:read"]
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
"scripts": {
|
||||
"build": "ng build table-plugin",
|
||||
"build:dev": "ng build table-plugin --configuration development",
|
||||
"watch": "ng build table-plugin --configuration development --watch",
|
||||
"serve": "ng serve table-plugin",
|
||||
"lint": "eslint .",
|
||||
"test": "vitest"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApplicationConfig } from '@angular/core';
|
||||
import { provideRouter } from '@angular/router';
|
||||
import { provideRouter, withHashLocation } from '@angular/router';
|
||||
import { appRoutes } from './app.routes';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [provideRouter(appRoutes)],
|
||||
providers: [provideRouter(appRoutes, withHashLocation())],
|
||||
};
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>table-plugin</title>
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
</head>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"name": "Table plugin",
|
||||
"description": "Table plugin to import or create tables",
|
||||
"code": "/assets/plugin.js",
|
||||
"icon": "/assets/icon.png",
|
||||
"version": 2,
|
||||
"code": "assets/plugin.js",
|
||||
"icon": "assets/icon.png",
|
||||
"permissions": ["content:read", "content:write"]
|
||||
}
|
||||
6
plugins/apps/table-plugin/vite.config.ts
Normal file
6
plugins/apps/table-plugin/vite.config.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/// <reference types="vitest/config" />
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
export default defineConfig({
|
||||
root: "./"
|
||||
});
|
||||
@@ -6,8 +6,8 @@
|
||||
"ses": "^1.1.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"module": "./index.mjs",
|
||||
"typings": "./index.d.ts",
|
||||
"module": "./dist/index.js",
|
||||
"typings": "./dist/index.d.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
|
||||
@@ -6,6 +6,7 @@ export const manifestSchema = z.object({
|
||||
host: z.string().url(),
|
||||
code: z.string(),
|
||||
icon: z.string().optional(),
|
||||
version: z.number().optional(),
|
||||
description: z.string().max(200).optional(),
|
||||
permissions: z.array(
|
||||
z.enum([
|
||||
|
||||
@@ -2,5 +2,5 @@ import { z } from 'zod';
|
||||
|
||||
export const openUISchema = z.object({
|
||||
width: z.number().positive(),
|
||||
height: z.number().positive(),
|
||||
height: z.number().positive()
|
||||
});
|
||||
|
||||
@@ -1,8 +1,28 @@
|
||||
import { Manifest } from './models/manifest.model.js';
|
||||
import { manifestSchema } from './models/manifest.schema.js';
|
||||
|
||||
export function getValidUrl(host: string, path: string): string {
|
||||
return new URL(path, host).toString();
|
||||
export function getValidUrl(host: string, path: string): URL {
|
||||
return new URL(path, host);
|
||||
}
|
||||
|
||||
export function prepareUrl(manifest: Manifest, url: string, params: Object): string {
|
||||
const result = getValidUrl(manifest.host, url);
|
||||
for (let [k, v] of Object.entries(params)) {
|
||||
if (!result.searchParams.has(k)) {
|
||||
result.searchParams.set(k, v);
|
||||
}
|
||||
}
|
||||
|
||||
if (manifest.version === undefined || manifest.version === 1) {
|
||||
return result.toString();
|
||||
} else if (manifest.version === 2) {
|
||||
const queryString = result.searchParams.toString();
|
||||
result.search = "";
|
||||
result.hash = `/?${queryString}`;
|
||||
return result.toString();
|
||||
} else {
|
||||
throw new Error("invalid manifest version");
|
||||
}
|
||||
}
|
||||
|
||||
export function loadManifest(url: string): Promise<Manifest> {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, it, vi, expect, beforeEach, afterEach } from 'vitest';
|
||||
import { createPluginManager } from './plugin-manager';
|
||||
import { loadManifestCode, getValidUrl } from './parse-manifest.js';
|
||||
import { loadManifestCode, getValidUrl, prepareUrl } from './parse-manifest.js';
|
||||
import { PluginModalElement } from './modal/plugin-modal.js';
|
||||
import { openUIApi } from './api/openUI.api.js';
|
||||
import type { Context, Theme } from '@penpot/plugin-types';
|
||||
@@ -9,6 +9,7 @@ import type { Manifest } from './models/manifest.model.js';
|
||||
vi.mock('./parse-manifest.js', () => ({
|
||||
loadManifestCode: vi.fn(),
|
||||
getValidUrl: vi.fn(),
|
||||
prepareUrl: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('./api/openUI.api.js', () => ({
|
||||
@@ -71,7 +72,8 @@ describe('createPluginManager', () => {
|
||||
vi.mocked(loadManifestCode).mockResolvedValue(
|
||||
'console.log("Plugin loaded");',
|
||||
);
|
||||
vi.mocked(getValidUrl).mockReturnValue('https://example.com/plugin');
|
||||
vi.mocked(getValidUrl).mockReturnValue(new URL('https://example.com/plugin'));
|
||||
vi.mocked(prepareUrl).mockReturnValue('https://example.com/plugin');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -110,7 +112,7 @@ describe('createPluginManager', () => {
|
||||
height: 300,
|
||||
});
|
||||
|
||||
expect(getValidUrl).toHaveBeenCalledWith(manifest.host, '/test-url');
|
||||
expect(prepareUrl).toHaveBeenCalledWith(manifest, '/test-url', {theme: "light"});
|
||||
expect(openUIApi).toHaveBeenCalledWith(
|
||||
'Test Modal',
|
||||
'https://example.com/plugin',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Context, Theme } from '@penpot/plugin-types';
|
||||
|
||||
import { getValidUrl, loadManifestCode } from './parse-manifest.js';
|
||||
import { prepareUrl, loadManifestCode } from './parse-manifest.js';
|
||||
import { Manifest } from './models/manifest.model.js';
|
||||
import { PluginModalElement } from './modal/plugin-modal.js';
|
||||
import { openUIApi } from './api/openUI.api.js';
|
||||
@@ -8,6 +8,7 @@ import { OpenUIOptions } from './models/open-ui-options.model.js';
|
||||
import { RegisterListener } from './models/plugin.model.js';
|
||||
import { openUISchema } from './models/open-ui-options.schema.js';
|
||||
|
||||
|
||||
export async function createPluginManager(
|
||||
context: Context,
|
||||
manifest: Manifest,
|
||||
@@ -80,9 +81,8 @@ export async function createPluginManager(
|
||||
};
|
||||
|
||||
const openModal = (name: string, url: string, options?: OpenUIOptions) => {
|
||||
const theme = context.theme as 'light' | 'dark';
|
||||
|
||||
const modalUrl = getValidUrl(manifest.host, url);
|
||||
const theme = context.theme as Theme;
|
||||
const modalUrl = prepareUrl(manifest, url, {theme});
|
||||
|
||||
if (modal?.getAttribute('iframe-src') === modalUrl) {
|
||||
return;
|
||||
|
||||
@@ -35,7 +35,7 @@ export default defineConfig({
|
||||
// Configuration for building your library.
|
||||
// See: https://vitejs.dev/guide/build.html#library-mode
|
||||
build: {
|
||||
outDir: '../../dist/plugins-runtime',
|
||||
outDir: './dist/',
|
||||
reportCompressedSize: true,
|
||||
commonjsOptions: {
|
||||
transformMixedEsModules: true,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.6.0",
|
||||
"type": "module",
|
||||
"license": "MIT",
|
||||
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8",
|
||||
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
||||
"scripts": {
|
||||
"start": "pnpm run start:app:runtime",
|
||||
"start:app:runtime": "concurrently --kill-others --names build,server \"pnpm --filter @penpot/plugins-runtime run build:watch\" \"pnpm --filter @penpot/plugins-runtime run preview\"",
|
||||
@@ -50,7 +50,6 @@
|
||||
"@vitest/coverage-v8": "4.0.17",
|
||||
"@vitest/ui": "4.0.17",
|
||||
"concurrently": "^9.2.1",
|
||||
"dotenv": "^17.2.4",
|
||||
"esbuild": "^0.27.2",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
|
||||
9
plugins/pnpm-lock.yaml
generated
9
plugins/pnpm-lock.yaml
generated
@@ -120,9 +120,6 @@ importers:
|
||||
concurrently:
|
||||
specifier: ^9.2.1
|
||||
version: 9.2.1
|
||||
dotenv:
|
||||
specifier: ^17.2.4
|
||||
version: 17.2.4
|
||||
esbuild:
|
||||
specifier: ^0.27.2
|
||||
version: 0.27.2
|
||||
@@ -3508,10 +3505,6 @@ packages:
|
||||
domutils@3.2.2:
|
||||
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
|
||||
|
||||
dotenv@17.2.4:
|
||||
resolution: {integrity: sha512-mudtfb4zRB4bVvdj0xRo+e6duH1csJRM8IukBqfTRvHotn9+LBXB8ynAidP9zHqoRC/fsllXgk4kCKlR21fIhw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -10222,8 +10215,6 @@ snapshots:
|
||||
domelementtype: 2.3.0
|
||||
domhandler: 5.0.3
|
||||
|
||||
dotenv@17.2.4: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
|
||||
Reference in New Issue
Block a user