Compare commits

..

1 Commits

Author SHA1 Message Date
Alejandro Alonso
0dda7bd9ee 🐛 Fix text stroke bounds 2026-02-17 12:25:32 +01:00
75 changed files with 1969 additions and 2238 deletions

View File

@@ -1,7 +1,6 @@
name: _DEVELOP
on:
workflow_dispatch:
schedule:
- cron: '16 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _STAGING RENDER
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _STAGING
on:
workflow_dispatch:
schedule:
- cron: '36 5-20 * * 1-5'

View File

@@ -1,7 +1,6 @@
name: _TAG
on:
workflow_dispatch:
push:
tags:
- '*'

View File

@@ -1,6 +1,6 @@
# CHANGELOG
## 2.15.0 (Unreleased)
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
@@ -10,20 +10,6 @@
### :sparkles: New features & Enhancements
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
### :bug: Bugs fixed
- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361)
## 2.14.0 (Unreleased)
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
@@ -31,6 +17,7 @@
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
- [Access Tokens] Look & feel refinement [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
### :bug: Bugs fixed
@@ -48,7 +35,6 @@
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
## 2.13.3

View File

@@ -53,7 +53,6 @@
::yres/status 200
::yres/body (yres/stream-body
(fn [_ output]
(let [channel (sp/chan :buf buf :xf (keep encode))
listener (events/spawn-listener
channel

View File

@@ -54,7 +54,7 @@
[:path ::fs/path]
[:mtype {:optional true} ::sm/text]])
(def check-input
(def ^:private check-input
(sm/check-fn schema:input))
(defn validate-media-type!
@@ -381,22 +381,6 @@
(when (zero? (:exit res))
(:out res))))
(woff2->sfnt [data]
;; woff2_decompress outputs to same directory with .ttf extension
(let [finput (tmp/tempfile :prefix "penpot.font." :suffix ".woff2")
foutput (fs/path (str/replace (str finput) #"\.woff2$" ".ttf"))]
(try
(io/write* finput data)
(let [res (sh/sh "woff2_decompress" (str finput))]
(if (zero? (:exit res))
foutput
(do
(when (fs/exists? foutput)
(fs/delete foutput))
nil)))
(finally
(fs/delete finput)))))
;; Documented here:
;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
(get-sfnt-type [data]
@@ -446,27 +430,4 @@
(= stype :ttf)
(-> (assoc "font/otf" (ttf->otf sfnt))
(assoc "font/ttf" sfnt)))))
(contains? current "font/woff2")
(let [data (get input "font/woff2")
foutput (woff2->sfnt data)]
(when-not foutput
(ex/raise :type :validation
:code :invalid-woff2-file
:hint "invalid woff2 file"))
(try
(let [sfnt (io/read* foutput)
type (get-sfnt-type sfnt)]
(cond-> input
(= type :otf)
(-> (assoc "font/otf" sfnt)
(assoc "font/ttf" (otf->ttf sfnt))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))
(= type :ttf)
(-> (assoc "font/ttf" sfnt)
(assoc "font/otf" (ttf->otf sfnt))
(update "font/woff" gen-if-nil #(ttf-or-otf->woff sfnt)))))
(finally
(fs/delete foutput))))))))
(assoc "font/ttf" sfnt)))))))))

View File

@@ -463,10 +463,8 @@
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile"
:fn mg0145/migrate}
:fn mg0145/migrate}])
{:name "0146-mod-access-token-table"
:fn (mg/resource "app/migrations/sql/0146-mod-access-token-table.sql")}])
(defn apply-migrations!
[pool name migrations]

View File

@@ -1,2 +0,0 @@
ALTER TABLE access_token
ADD COLUMN type text NULL;

View File

@@ -73,13 +73,9 @@
(if (nil? result)
204
200))
headers (::http/headers mdata {})
headers (cond-> headers
(and (yres/stream-body? result)
(not (contains? headers "content-type")))
headers (cond-> (::http/headers mdata {})
(yres/stream-body? result)
(assoc "content-type" "application/octet-stream"))]
{::yres/status status
::yres/headers headers
::yres/body result}))]

View File

@@ -23,7 +23,7 @@
(dissoc row :perms))
(defn create-access-token
[{:keys [::db/conn] :as cfg} profile-id name expiration type]
[{:keys [::db/conn] :as cfg} profile-id name expiration]
(let [token-id (uuid/next)
expires-at (some-> expiration (ct/in-future))
created-at (ct/now)
@@ -36,7 +36,6 @@
{:id token-id
:name name
:token token
:type type
:profile-id profile-id
:created-at created-at
:updated-at created-at
@@ -51,18 +50,17 @@
(def ^:private schema:create-access-token
[:map {:title "create-access-token"}
[:name [:string {:max 250 :min 1}]]
[:expiration {:optional true} ::ct/duration]
[:type {:optional true} :string]])
[:expiration {:optional true} ::ct/duration]])
(sv/defmethod ::create-access-token
{::doc/added "1.18"
::sm/params schema:create-access-token}
[cfg {:keys [::rpc/profile-id name expiration type]}]
[cfg {:keys [::rpc/profile-id name expiration]}]
(quotes/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id})
(db/tx-run! cfg create-access-token profile-id name expiration type))
(db/tx-run! cfg create-access-token profile-id name expiration))
(def ^:private schema:delete-access-token
[:map {:title "delete-access-token"}
@@ -85,22 +83,5 @@
(->> (db/query pool :access-token
{:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:id :name :perms :type :created-at :updated-at :expires-at]})
:columns [:id :name :perms :created-at :updated-at :expires-at]})
(mapv decode-row)))
(def ^:private schema:get-current-mcp-token
[:map {:title "get-current-mcp-token"}])
(sv/defmethod ::get-current-mcp-token
{::doc/added "2.15"
::sm/params schema:get-current-mcp-token}
[{:keys [::db/pool]} {:keys [::rpc/profile-id ::rpc/request-at]}]
(->> (db/query pool :access-token
{:profile-id profile-id
:type "mcp"}
{:order-by [[:expires-at :asc] [:created-at :asc]]
:columns [:token :expires-at]})
(remove #(ct/is-after? (:expires-at %) request-at))
(map decode-row)
(first)))

View File

@@ -9,14 +9,12 @@
[app.binfile.common :as bfc]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.media :as cmedia]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.db :as db]
[app.db.sql :as-alias sql]
[app.features.logical-deletion :as ldel]
[app.http :as-alias http]
[app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks]
[app.media :as media]
@@ -36,9 +34,7 @@
java.io.InputStream
java.io.OutputStream
java.io.SequenceInputStream
java.util.Collections
java.util.zip.ZipEntry
java.util.zip.ZipOutputStream))
java.util.Collections))
(set! *warn-on-reflection* true)
@@ -300,98 +296,3 @@
(rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant)
:font-id (:font-id variant)}})))
;; --- DOWNLOAD FONT
(defn- make-temporal-storage-object
[cfg profile-id content]
(let [storage (sto/resolve cfg)
content (media/check-input content)
hash (sto/calculate-hash (:path content))
data (-> (sto/content (:path content))
(sto/wrap-with-hash hash))
mtype (:mtype content "application/octet-stream")
content {::sto/content data
::sto/deduplicate? true
::sto/touched-at (ct/in-future {:minutes 30})
:profile-id profile-id
:content-type mtype
:bucket "tempfile"}]
(sto/put-object! storage content)))
(defn- make-variant-filename
[v mtype]
(str (:font-family v) "-" (:font-weight v)
(when-not (= "normal" (:font-style v)) (str "-" (:font-style v)))
(cmedia/mtype->extension mtype)))
(def ^:private schema:download-font
[:map {:title "download-font"}
[:id ::sm/uuid]])
(sv/defmethod ::download-font
"Download the font file. Returns a http redirect to the asset resource uri."
{::doc/added "2.15"
::sm/params schema:download-font}
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id id]}]
(let [variant (db/get pool :team-font-variant {:id id})]
(teams/check-read-permissions! pool profile-id (:team-id variant))
;; Try to get the best available font format (prefer TTF for broader compatibility).
(let [media-id (or (:ttf-file-id variant)
(:otf-file-id variant)
(:woff2-file-id variant)
(:woff1-file-id variant))
sobj (sto/get-object storage media-id)
mtype (-> sobj meta :content-type)]
{:id (:id sobj)
:uri (files/resolve-public-uri (:id sobj))
:name (make-variant-filename variant mtype)})))
(def ^:private schema:download-font-family
[:map {:title "download-font-family"}
[:font-id ::sm/uuid]])
(sv/defmethod ::download-font-family
"Download the entire font family as a zip file. Returns the zip
bytes on the body, without encoding it on transit or json."
{::doc/added "2.15"
::sm/params schema:download-font-family}
[{:keys [::sto/storage ::db/pool] :as cfg} {:keys [::rpc/profile-id font-id]}]
(let [variants (db/query pool :team-font-variant
{:font-id font-id
:deleted-at nil})]
(when-not (seq variants)
(ex/raise :type :not-found
:code :object-not-found))
(teams/check-read-permissions! pool profile-id (:team-id (first variants)))
(let [tempfile (tmp/tempfile :suffix ".zip")
ffamily (-> variants first :font-family)]
(with-open [^OutputStream output (io/output-stream tempfile)
^OutputStream output (ZipOutputStream. output)]
(doseq [v variants]
(let [media-id (or (:ttf-file-id v)
(:otf-file-id v)
(:woff2-file-id v)
(:woff1-file-id v))
sobj (sto/get-object storage media-id)
mtype (-> sobj meta :content-type)
name (make-variant-filename v mtype)]
(with-open [input (sto/get-object-data storage sobj)]
(.putNextEntry ^ZipOutputStream output (ZipEntry. ^String name))
(io/copy input output :size (:size sobj))
(.closeEntry ^ZipOutputStream output)))))
(let [{:keys [id] :as sobj} (make-temporal-storage-object cfg profile-id
{:mtype "application/zip"
:path tempfile})]
{:id id
:uri (files/resolve-public-uri id)
:name (str ffamily ".zip")}))))

View File

@@ -48,7 +48,6 @@
(def schema:props
[:map {:title "ProfileProps"}
[:plugins {:optional true} schema:plugin-registry]
[:mcp-status {:optional true} ::sm/boolean]
[:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean]
[:onboarding-team-id {:optional true} ::sm/uuid]

View File

@@ -102,7 +102,7 @@
(t/deftest access-token-authz
(let [profile (th/create-profile* 1)
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil nil)
token (db/tx-run! th/*system* app.rpc.commands.access-token/create-access-token (:id profile) "test" nil)
handler (#'app.http.access-token/wrap-authz identity th/*system*)]
(let [response (handler nil)]

View File

@@ -107,18 +107,4 @@
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [results (:result out)]
(t/is (= 2 (count results))))))
(t/testing "get mcp token"
(let [_ (th/command! {::th/type :create-access-token
::rpc/profile-id (:id prof)
:type "mcp"
:name "token 1"
:perms ["get-profile"]})
{:keys [error result]}
(th/command! {::th/type :get-current-mcp-token
::rpc/profile-id (:id prof)})]
;; (th/print-result! result)
(t/is (nil? error))
(t/is (string? (:token result)))))))
(t/is (= 2 (count results))))))))

View File

@@ -93,41 +93,6 @@
:font-weight
:font-style))))
(t/deftest woff2-font-upload-1
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)
proj-id (:default-project-id prof)
font-id (uuid/custom 10 1)
data (-> (io/resource "backend_tests/test_files/font-1.woff2")
(io/read*))
params {::th/type :create-font-variant
::rpc/profile-id (:id prof)
:team-id team-id
:font-id font-id
:font-family "somefont"
:font-weight 400
:font-style "normal"
:data {"font/woff2" data}}
out (th/command! params)]
;; (th/print-result! out)
(t/is (nil? (:error out)))
(let [result (:result out)]
(t/is (uuid? (:id result)))
(t/is (uuid? (:ttf-file-id result)))
(t/is (uuid? (:otf-file-id result)))
(t/is (uuid? (:woff1-file-id result)))
(t/is (uuid? (:woff2-file-id result)))
(t/are [k] (= (get params k)
(get result k))
:team-id
:font-id
:font-family
:font-weight
:font-style))))
(t/deftest font-deletion-1
(let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof)

View File

Binary file not shown.

View File

@@ -152,9 +152,7 @@
:redis-cache
;; Activates the nitrate module
:nitrate
:mcp})
:nitrate})
(def all-flags
(set/union email login varia))

View File

@@ -12,7 +12,6 @@
(def font-types
#{"font/ttf"
"font/woff"
"font/woff2"
"font/otf"
"font/opentype"})
@@ -82,22 +81,21 @@
(defn parse-font-weight
[variant]
(cond
(re-seq #"(?i)(?:^|[-_\s])(hairline|thin)(?=(?:[-_\s]|$|italic\b))" variant) 100
(re-seq #"(?i)(?:^|[-_\s])(extra\s*light|ultra\s*light)(?=(?:[-_\s]|$|italic\b))" variant) 200
(re-seq #"(?i)(?:^|[-_\s])(light)(?=(?:[-_\s]|$|italic\b))" variant) 300
(re-seq #"(?i)(?:^|[-_\s])(normal|regular)(?=(?:[-_\s]|$|italic\b))" variant) 400
(re-seq #"(?i)(?:^|[-_\s])(medium)(?=(?:[-_\s]|$|italic\b))" variant) 500
(re-seq #"(?i)(?:^|[-_\s])(semi\s*bold|demi\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 600
(re-seq #"(?i)(?:^|[-_\s])(extra\s*bold|ultra\s*bold)(?=(?:[-_\s]|$|italic\b))" variant) 800
(re-seq #"(?i)(?:^|[-_\s])(bold)(?=(?:[-_\s]|$|italic\b))" variant) 700
(re-seq #"(?i)(?:^|[-_\s])(extra\s*black|ultra\s*black)(?=(?:[-_\s]|$|italic\b))" variant) 950
(re-seq #"(?i)(?:^|[-_\s])(black|heavy|solid)(?=(?:[-_\s]|$|italic\b))" variant) 900
:else 400))
(re-seq #"(?i)(?:hairline|thin)" variant) 100
(re-seq #"(?i)(?:extra\s*light|ultra\s*light)" variant) 200
(re-seq #"(?i)(?:light)" variant) 300
(re-seq #"(?i)(?:normal|regular)" variant) 400
(re-seq #"(?i)(?:medium)" variant) 500
(re-seq #"(?i)(?:semi\s*bold|demi\s*bold)" variant) 600
(re-seq #"(?i)(?:extra\s*bold|ultra\s*bold)" variant) 800
(re-seq #"(?i)(?:bold)" variant) 700
(re-seq #"(?i)(?:extra\s*black|ultra\s*black)" variant) 950
(re-seq #"(?i)(?:black|heavy|solid)" variant) 900
:else 400))
(defn parse-font-style
[variant]
(if (or (re-seq #"(?i)(?:^|[-_\s])(italic)(?:[-_\s]|$)" variant)
(re-seq #"(?i)italic$" variant))
(if (re-seq #"(?i)(?:italic)" variant)
"italic"
"normal"))

View File

@@ -9,39 +9,6 @@
[app.common.media :as media]
[clojure.test :as t]))
(t/deftest test-parse-font-weight
(t/testing "matches weight tokens with proper boundaries"
(t/is (= 700 (media/parse-font-weight "Roboto-Bold")))
(t/is (= 700 (media/parse-font-weight "Roboto_Bold")))
(t/is (= 700 (media/parse-font-weight "Roboto Bold")))
(t/is (= 700 (media/parse-font-weight "Bold")))
(t/is (= 800 (media/parse-font-weight "Roboto-ExtraBold")))
(t/is (= 600 (media/parse-font-weight "OpenSans-SemiBold")))
(t/is (= 300 (media/parse-font-weight "Lato-Light")))
(t/is (= 100 (media/parse-font-weight "Roboto-Thin")))
(t/is (= 200 (media/parse-font-weight "Roboto-ExtraLight")))
(t/is (= 500 (media/parse-font-weight "Roboto-Medium")))
(t/is (= 900 (media/parse-font-weight "Roboto-Black"))))
(t/testing "does not match weight tokens embedded in words"
(t/is (= 400 (media/parse-font-weight "Boldini")))
(t/is (= 400 (media/parse-font-weight "Lighthaus")))
(t/is (= 400 (media/parse-font-weight "Blackwood")))
(t/is (= 400 (media/parse-font-weight "Thinker")))
(t/is (= 400 (media/parse-font-weight "Mediaeval")))))
(t/deftest test-parse-font-style
(t/testing "matches italic with proper boundaries"
(t/is (= "italic" (media/parse-font-style "Roboto-Italic")))
(t/is (= "italic" (media/parse-font-style "Roboto_Italic")))
(t/is (= "italic" (media/parse-font-style "Roboto Italic")))
(t/is (= "italic" (media/parse-font-style "Italic")))
(t/is (= "italic" (media/parse-font-style "Roboto-BoldItalic"))))
(t/testing "does not match italic embedded in words"
(t/is (= "normal" (media/parse-font-style "Italica")))
(t/is (= "normal" (media/parse-font-style "Roboto-Regular")))))
(t/deftest test-strip-image-extension
(t/testing "removes extension from supported image files"
(t/is (= (media/strip-image-extension "foo.png") "foo"))

View File

@@ -42,13 +42,12 @@
"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\"",
"postinstall": "(cd ../plugins/libs/plugins-runtime; pnpm install; pnpm run build)"
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
},
"devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap",
"@penpot/plugins-runtime": "link:../plugins/dist/plugins-runtime",
"@penpot/plugins-runtime": "1.4.2",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor",
"@penpot/tokenscript": "workspace:./packages/tokenscript",

View File

@@ -20,8 +20,8 @@ importers:
specifier: workspace:./packages/mousetrap
version: link:packages/mousetrap
'@penpot/plugins-runtime':
specifier: link:../plugins/dist/plugins-runtime
version: link:../plugins/dist/plugins-runtime
specifier: 1.4.2
version: 1.4.2
'@penpot/svgo':
specifier: penpot/svgo#v3.2
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
@@ -581,6 +581,15 @@ 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'}
@@ -1249,6 +1258,12 @@ 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'}
@@ -4621,6 +4636,9 @@ 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'}
@@ -5481,6 +5499,9 @@ 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==}
@@ -5754,6 +5775,12 @@ 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
@@ -6270,6 +6297,14 @@ 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
@@ -9965,6 +10000,12 @@ 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
@@ -10933,4 +10974,6 @@ snapshots:
dependencies:
zod: 4.3.6
zod@3.25.76: {}
zod@4.3.6: {}

View File

@@ -119,10 +119,6 @@
(normalize-uri (or (obj/get global "penpotPublicURI")
(obj/get location "origin"))))
(def mcp-ws-uri
(or (some-> (obj/get global "penpotMcpServerURI") u/uri)
(u/join public-uri "mcp/ws")))
(def rasterizer-uri
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
public-uri))
@@ -151,9 +147,6 @@
(let [f (obj/get global "initializeExternalConfigInfo")]
(when (fn? f) (f))))
(def mcp-server-url (-> public-uri u/ensure-path-slash (u/join "mcp") str))
(def mcp-help-center-uri "https://help.penpot.app/technical-guide/")
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]

View File

@@ -99,65 +99,46 @@
map with temporal ID's associated to each font entry."
[blobs team-id]
(letfn [(prepare [{:keys [font type name data] :as params}]
(if font
;; Font was parsed with opentype.js (ttf, otf, woff)
(let [family (or (.getEnglishName ^js font "preferredFamily")
(.getEnglishName ^js font "fontFamily"))
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))
(let [family (or (.getEnglishName ^js font "preferredFamily")
(.getEnglishName ^js font "fontFamily"))
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))
;; Vertical metrics determine the baseline in a text and the space between lines of
;; text. For historical reasons, there are three pairs of ascender/descender
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
;; system and application a different set will be used to render text on the
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
;; table. On Windows, all browsers use the usWin metrics, but respect the
;; useTypoMetrics setting and if set will use the OS/2 values.
;; Vertical metrics determine the baseline in a text and the space between lines of
;; text. For historical reasons, there are three pairs of ascender/descender
;; values, known as hhea, OS/2 and uSWin metrics. Depending on the font, operating
;; system and application a different set will be used to render text on the
;; screen. On Mac, Safari and Chrome use the hhea values to render text. Firefox
;; will respect the useTypoMetrics setting and will use the OS/2 if it is set. If
;; the useTypoMetrics is not set, Firefox will also use metrics from the hhea
;; table. On Windows, all browsers use the usWin metrics, but respect the
;; useTypoMetrics setting and if set will use the OS/2 values.
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
;; useTypoMetrics can be read from the 7th bit
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
;; useTypoMetrics can be read from the 7th bit
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
height-warning? (or (not= hhea-ascender win-ascent)
(not= hhea-descender win-descent)
(and f-selection (or
(not= hhea-ascender os2-ascent)
(not= hhea-descender os2-descent))))
data (js/Uint8Array. data)]
{:content {:data (chunk-array data default-chunk-size)
:name name
:type type}
:font-family (or family "")
:font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant)
:height-warning? height-warning?})
;; Font could not be parsed (woff2), extract metadata from filename
(let [base-name (str/replace name #"\.[^.]+$" "")
;; Strip known weight/style tokens and separators to derive family name
;; Use word boundaries to avoid matching substrings (e.g. "Boldini" should not match "bold")
raw-family-name (-> base-name
(str/replace #"(?i)(^|[-_\s])(extra\s*black|ultra\s*black|extra\s*bold|ultra\s*bold|semi\s*bold|demi\s*bold|extra\s*light|ultra\s*light|hairline|thin|light|normal|regular|medium|bold|black|heavy|solid|italic)([-_\s]|$)" "$1$3")
(str/replace #"[-_\s]+" " ")
(str/trim))
family-name (if (str/blank? raw-family-name) base-name raw-family-name)
data (js/Uint8Array. data)]
{:content {:data (chunk-array data default-chunk-size)
:name name
:type type}
:font-family family-name
:font-weight (cm/parse-font-weight base-name)
:font-style (cm/parse-font-style base-name)
:height-warning? false})))
height-warning? (or (not= hhea-ascender win-ascent)
(not= hhea-descender win-descent)
(and f-selection (or
(not= hhea-ascender os2-ascent)
(not= hhea-descender os2-descent))))
data (js/Uint8Array. data)]
{:content {:data (chunk-array data default-chunk-size)
:name name
:type type}
:font-family (or family "")
:font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant)
:height-warning? height-warning?}))
(join [res {:keys [content] :as font}]
(let [key-fn (juxt :font-family :font-weight :font-style)
@@ -185,18 +166,14 @@
(case sg
"117 124 124 117" "font/otf"
"0 1 0 0" "font/ttf"
"167 117 106 106" "font/woff"
"167 117 106 62" "font/woff2")))
"167 117 106 106" "font/woff")))
(parse-font [{:keys [data type name] :as params}]
(if (= type "font/woff2")
;; woff2 cannot be parsed by opentype.js, extract metadata from filename
(assoc params :font nil)
(try
(assoc params :font (ot/parse data))
(catch :default _e
(log/warn :msg (str/fmt "skipping file %s, unsupported format" name))
nil))))
(parse-font [{:keys [data] :as params}]
(try
(assoc params :font (ot/parse data))
(catch :default _e
(log/warn :msg (str/fmt "skipping file %s, unsupported format" (:name params)))
nil)))
(read-blob [blob]
(->> (wa/read-file-as-array-buffer blob)

View File

@@ -65,23 +65,8 @@
(update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
(defn start-plugin!
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
(.ɵloadPlugin
^js ug/global
#js {:pluginId plugin-id
:name name
:version version
:description description
:host host
:code code
:allowBackground (boolean allow-background)
:permissions (apply array permissions)}
nil
extensions))
(defn- load-plugin!
[{:keys [plugin-id name description host code icon permissions] :as params}]
[{:keys [plugin-id name description host code icon permissions]}]
(try
(st/emit! (save-current-plugin plugin-id)
(reset-plugin-flags plugin-id))

View File

@@ -498,3 +498,4 @@
(->> (rp/cmd! :delete-access-token params)
(rx/tap on-success)
(rx/catch on-error))))))

View File

@@ -52,7 +52,6 @@
[app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl]
[app.main.data.workspace.mcp :as mcp]
[app.main.data.workspace.notifications :as dwn]
[app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.path :as dwdp]
@@ -213,8 +212,7 @@
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id)
(mcp/init-mcp-connexion)))))
(fdf/fix-deleted-fonts-for-local-library file-id)))))
(defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}]
@@ -1448,7 +1446,6 @@
(dm/export dwcp/paste-shapes)
(dm/export dwcp/paste-data-valid?)
(dm/export dwcp/copy-link-to-clipboard)
(dm/export dwcp/copy-as-image)
;; Drawing
(dm/export dwd/select-for-drawing)

View File

@@ -1039,55 +1039,3 @@
ptk/WatchEvent
(watch [_ _ _]
(clipboard/to-clipboard (rt/get-current-href)))))
(defn copy-as-image
[]
(ptk/reify ::copy-as-image
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
page-id (:current-page-id state)
selected (first (dsh/lookup-selected state))
export {:file-id file-id
:page-id page-id
:object-id selected
;; webp would be preferrable, but PNG is the most supported image MIME type by clipboard APIs.
:type :png
;; Always use 2 to ensure good enough quality for wireframes.
:scale 2
:suffix ""
:enabled true
:name ""}
params {:exports [export]
:profile-id (:profile-id state)
:cmd :export-shapes
:wait true}]
(rx/concat
;; Ensure current state persisted before exporting.
(rx/of ::dps/force-persist)
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/first)
(rx/timeout 400 (rx/empty)))
;; Exporting itself can time its time, better to notify that we are busy.
(rx/of (ntf/info (tr "workspace.clipboard.copying")))
;; Call exporter to get image URI, then fetch and copy blob.
(->> (rp/cmd! :export params)
(rx/mapcat (fn [{:keys [uri]}]
(http/send! {:method :get
:uri uri
:response-type :blob})))
(rx/map :body)
(rx/tap (fn [blob]
(clipboard/to-clipboard-promise "image/png" (p/resolved blob))))
(rx/map (fn [_]
(ntf/success (tr "workspace.clipboard.image-copied"))))
(rx/catch (fn [e]
(js/console.error "clipboard blocked:" e)
(ntf/error (tr "workspace.clipboard.image-copy-failed"))
(rx/empty)))))))))

View File

@@ -6,7 +6,6 @@
(ns app.main.data.workspace.drawing.box
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
@@ -29,9 +28,9 @@
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn- adjust-ratio
(defn adjust-ratio
[point initial]
(let [v (gpt/to-vec point initial)
(let [v (gpt/to-vec point initial)
dx (mth/abs (:x v))
dy (mth/abs (:y v))
sx (mth/sign (:x v))
@@ -44,43 +43,32 @@
(> dy dx)
(assoc :x (- (:x point) (* sx (- dy dx)))))))
(defn- resize-shape
[{:keys [x y width height] :as shape} initial point lock? mod? snap-pixel?]
(defn resize-shape [{:keys [x y width height] :as shape} initial point lock? mod? snap-pixel?]
(if (and (some? x) (some? y) (some? width) (some? height))
(let [p2
(cond-> point lock? (adjust-ratio initial))
(let [draw-rect (cond-> (grc/make-rect initial (cond-> point lock? (adjust-ratio initial)))
snap-pixel?
(-> (update :width max 1)
(update :height max 1)))
p1
(if mod?
(gpt/point (- (* 2 (:x initial)) (:x p2))
(- (* 2 (:y initial)) (:y p2)))
initial)
shape-rect (grc/make-rect x y width height)
draw-rect
(cond-> (grc/make-rect p1 p2)
snap-pixel?
(-> (update :width d/max 1)
(update :height d/max 1)))
scalev (gpt/point (/ (:width draw-rect)
(:width shape-rect))
(/ (:height draw-rect)
(:height shape-rect)))
shape-rect
(grc/make-rect x y width height)
scalev
(gpt/point (/ (:width draw-rect) (:width shape-rect))
(/ (:height draw-rect) (:height shape-rect)))
movev
(gpt/to-vec (gpt/point shape-rect) (gpt/point draw-rect))]
movev (gpt/to-vec (gpt/point shape-rect)
(gpt/point draw-rect))]
(-> shape
(assoc :click-draw? false)
(vary-meta merge {:mod? mod?})
(gsh/transform-shape (-> (ctm/empty)
(ctm/resize scalev (gpt/point x y))
(ctm/move movev)))))
shape))
(defn- update-drawing
[state initial point lock? mod? snap-pixel?]
(defn- update-drawing [state initial point lock? mod? snap-pixel?]
(update-in state [:workspace-drawing :object] resize-shape initial point lock? mod? snap-pixel?))
(defn move-drawing
@@ -140,7 +128,7 @@
;; Take until before the snap calculation otherwise we could cancel the snap in the worker
;; and its a problem for fast moving drawing
(rx/take-until stopper)
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-alt)
(rx/with-latest-from ms/mouse-position-shift ms/mouse-position-mod)
(rx/switch-map
(fn [[point :as current]]
(->> (snap/closest-snap-point page-id [shape] objects layout zoom focus point)

View File

@@ -1,58 +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.data.workspace.mcp
(:require
[app.common.logging :as log]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.plugins :as dp]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(log/set-level! :info)
(def ^:private default-manifest
{:code "plugin.js"
:name "Penpot MCP Plugin"
:version 2
:plugin-id "96dfa740-005d-8020-8007-55ede24a2bae"
:description "This plugin enables interaction with the Penpot MCP server"
:allow-background true
:permissions
#{"library:read" "library:write" "comment:read" "content:write" "comment:write"
"content:read"}})
(defn init-mcp!
[]
(->> (rp/cmd! :get-current-mcp-token)
(rx/subs!
(fn [{:keys [token]}]
(when token
(dp/start-plugin!
(assoc default-manifest
:url (str (u/join cf/public-uri "plugins/mcp/manifest.json"))
:host (str (u/join cf/public-uri "plugins/mcp/")))
;; API extension for MCP server
#js {:mcp
#js
{:getToken (constantly token)
:getServerUrl #(str cf/mcp-ws-uri)
:setMcpStatus
(fn [status]
;; TODO: Visual feedback
(log/info :hint "MCP STATUS" :status status))}}))))))
(defn init-mcp-connexion
[]
(ptk/reify ::init-mcp-connexion
ptk/EffectEvent
(effect [_ state _]
(when (and (contains? cf/flags :mcp)
(-> state :profile :props :mcp-status))
(init-mcp!)))))

View File

@@ -197,7 +197,7 @@
:settings-options
:settings-feedback
:settings-subscription
:settings-integrations
:settings-access-tokens
:settings-notifications)
(let [params (get params :query)
error-report-id (some-> params :error-report-id uuid/parse*)]

View File

@@ -78,7 +78,7 @@
(kbd/enter? event)
(let [selected (dom/get-active)]
(dom/prevent-default event)
(dom/click selected))
(dom/click! selected))
(kbd/tab? event)
(on-close)))))]

View File

@@ -32,7 +32,6 @@
input-name (get props :name)
more-classes (get props :class)
auto-focus? (get props :auto-focus? false)
input-ref (mf/use-ref nil)
data-testid (d/nilv data-testid input-name)
@@ -83,6 +82,7 @@
(swap! form assoc-in [:touched input-name] true)
(fm/on-input-change form input-name value trim)
(on-change-value name value)))
on-blur
(fn [_]
(reset! focus? false))
@@ -92,18 +92,9 @@
(when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true)))
on-key-press
(mf/use-fn
(mf/deps input-ref)
(fn [e]
(dom/prevent-default e)
(when (kbd/space? e)
(dom/click (mf/ref-val input-ref)))))
props (-> props
(dissoc :help-icon :form :trim :children :show-success? :auto-focus? :label)
(assoc :id (name input-name)
:ref input-ref
:value value
:auto-focus auto-focus?
:on-click (when (or is-radio? is-checkbox?) on-click)
@@ -140,7 +131,7 @@
:for (name input-name)} label
(when is-checkbox?
[:span {:class (stl/css-case :global/checked checked?) :tab-index "0" :on-key-press on-key-press} (when checked? deprecated-icon/status-tick)])
[:span {:class (stl/css-case :global/checked checked?)} (when checked? deprecated-icon/status-tick)])
(if is-checkbox?
[:> :input props]

View File

@@ -9,17 +9,14 @@
(:require
[app.main.data.modal :as modal]
[app.main.store :as st]
[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 :refer [icon*]]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k]
[goog.events :as events]
[rumext.v2 :as mf])
(:import
goog.events.EventType))
(:import goog.events.EventType))
(mf/defc confirm-dialog
{::mf/register modal/components
@@ -71,11 +68,8 @@
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} title]
[:div {:class (stl/css :modal-close-btn)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click cancel-fn
:icon i/close}]]]
[:button {:class (stl/css :modal-close-btn)
:on-click cancel-fn} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
(when (and (string? message) (not= message ""))
@@ -93,19 +87,24 @@
[:ul {:class (stl/css :component-list)}
(for [item items]
[:li {:class (stl/css :modal-item-element)}
[:> icon* {:icon-id i/component
:class (stl/css :modal-component-icon)
:size "s"}]
[:span {:class (stl/css :modal-component-icon)}
deprecated-icon/component]
[:span {:class (stl/css :modal-component-name)}
(:name item)]])]])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
(when-not (= cancel-label :omit)
[:> button* {:variant "secondary"
:on-click cancel-fn}
cancel-label])
[:> button* {:variant (cond (= accept-style :danger) "destructive"
(= accept-style :primary) "primary")
:on-click accept-fn}
accept-label]]]]]))
[:input
{:class (stl/css :cancel-button)
:type "button"
:value cancel-label
:on-click cancel-fn}])
[:input
{:class (stl/css-case :accept-btn true
:danger (= accept-style :danger)
:primary (= accept-style :primary))
:type "button"
:value accept-label
:on-click accept-fn}]]]]]))

View File

@@ -15,9 +15,10 @@
.modal-container {
@extend .modal-container-base;
display: flex;
flex-direction: column;
gap: var(--sp-xxl);
}
.modal-header {
margin-bottom: deprecated.$s-24;
}
.modal-title {
@@ -26,13 +27,12 @@
}
.modal-close-btn {
position: absolute;
top: var(--sp-m);
right: var(--sp-m);
@extend .modal-close-btn-base;
}
.modal-content {
@include deprecated.bodyLargeTypography;
margin-bottom: deprecated.$s-24;
}
.modal-item-element {
@@ -41,18 +41,32 @@
.modal-component-icon {
@include deprecated.flexCenter;
color: var(--color-foreground-secondary);
height: deprecated.$s-16;
width: deprecated.$s-16;
svg {
@extend .button-icon-small;
stroke: var(--color);
}
}
.modal-component-name {
@include deprecated.bodyLargeTypography;
color: var(--color-foreground-secondary);
}
.action-buttons {
@extend .modal-action-btns;
}
.cancel-button {
@extend .modal-cancel-btn;
}
.accept-btn {
@extend .modal-accept-btn;
&.danger {
@extend .modal-danger-btn;
}
}
.modal-scd-msg,
.modal-subtitle,
.modal-msg {

View File

@@ -7,9 +7,7 @@
(ns app.main.ui.dashboard.fonts
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.exceptions :as ex]
[app.common.media :as cm]
[app.common.uuid :as uuid]
[app.config :as cf]
@@ -24,7 +22,6 @@
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.dom :as dom]
[app.util.http :as http]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[beicon.v2.core :as rx]
@@ -35,7 +32,7 @@
(def ^:private accept-font-types
(str (str/join "," cm/font-types)
;; A workaround to solve a problem with chrome input selector
",.ttf,application/font-woff,.woff,.woff2,.otf"))
",.ttf,application/font-woff,woff,.otf"))
(defn- use-page-title
[team section]
@@ -119,10 +116,10 @@
(swap! fonts* dissoc id)
(swap! uploading* disj id)
(st/emit! (df/add-font font)))
(fn [cause]
(fn [error]
(st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
(swap! fonts* dissoc id)
(ex/print-throwable cause))))))
(js/console.log "error" error))))))
on-upload
(mf/use-fn
@@ -262,14 +259,11 @@
(mf/defc installed-font-context-menu
{::mf/props :obj
::mf/private true}
[{:keys [is-open on-close on-edit on-download on-delete]}]
(let [options (mf/with-memo [on-edit on-download on-delete]
[{:keys [is-open on-close on-edit on-delete]}]
(let [options (mf/with-memo [on-edit on-delete]
[{:name (tr "labels.edit")
:id "font-edit"
:handler on-edit}
{:name (tr "labels.download-simple")
:id "font-download"
:handler on-download}
{:name (tr "labels.delete")
:id "font-delete"
:handler on-delete}])]
@@ -351,26 +345,6 @@
(st/emit! (df/delete-font font-id)))}]
(st/emit! (modal/show options)))))
on-download
(mf/use-fn
(mf/deps variants)
(fn [_event]
(let [variant (first variants)
variant-id (:id variant)
multiple? (> (count variants) 1)
cmd (if multiple? :download-font-family :download-font)
params (if multiple? {:font-id font-id} {:id variant-id})]
(->> (rp/cmd! cmd params)
(rx/mapcat (fn [{:keys [name uri]}]
(->> (http/send! {:uri uri :method :get :response-type :blob})
(rx/map :body)
(rx/map (fn [blob] (d/vec2 name blob))))))
(rx/subs! (fn [[filename blob]]
(dom/trigger-download filename blob))
(fn [error]
(js/console.error "error downloading font" error)
(st/emit! (ntf/error (tr "errors.download-font")))))))))
on-delete-variant
(mf/use-fn
(fn [event]
@@ -433,7 +407,6 @@
{:on-close on-menu-close
:is-open menu-open?
:on-delete on-delete-font
:on-download on-download
:on-edit on-edit}]]))]))
(mf/defc installed-fonts*

View File

@@ -77,7 +77,7 @@
(mf/use-ref nil)
on-import-files
(fn [] (dom/click (mf/ref-val file-input)))
(fn [] (dom/click! (mf/ref-val file-input)))
on-finish-import
(mf/use-fn

View File

@@ -30,13 +30,10 @@
[app.main.ui.dashboard.subscription :refer [dashboard-cta*
get-subscription-type
menu-team-icon*
nitrate-sidebar*
show-subscription-dashboard-banner?
subscription-sidebar*]]
[app.main.ui.dashboard.team-form]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.nitrate.nitrate-form]
[app.util.dom :as dom]
@@ -77,8 +74,6 @@
(def ^:private exit-icon
(deprecated-icon/icon-xref :exit (stl/css :exit-icon)))
(def ^:private ^:svg-id penpot-logo-icon "penpot-logo-icon")
(mf/defc sidebar-project*
{::mf/private true}
[{:keys [item is-selected]}]
@@ -502,23 +497,18 @@
(mf/defc sidebar-org-switch*
[{:keys [team profile]}]
(let [teams (mf/deref refs/teams)
orgs (mf/with-memo [teams]
(let [orgs (->> teams
vals
(group-by :organization-id)
(map (fn [[_group entries]] (first entries)))
vec
(d/index-by :id))]
(update-vals orgs
(fn [t]
(assoc t :name (str "ORG: " (:organization-name t)))))))
(let [teams (->> (mf/deref refs/teams)
vals
(group-by :organization-id)
(map (fn [[_group entries]] (first entries)))
vec
(d/index-by :id))
empty? (= (count orgs) 1)
teams (update-vals teams
(fn [t]
(assoc t :name (str "ORG: " (:organization-name t)))))
current-org (mf/with-memo [team]
(assoc team :name (str "ORG: " (:organization-name team))))
team (assoc team :name (str "ORG: " (:organization-name team)))
show-teams-menu*
(mf/use-state false)
@@ -540,53 +530,36 @@
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click)))))
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))
(mf/use-fn #(reset! show-teams-menu* false))]
on-create-org-click
(mf/use-fn
(fn []
(if (:nitrate-licence profile)
;; TODO update when org creation route is ready
(dom/open-new-window "/control-center/org/create")
(st/emit! (modal/show :nitrate-form {})))))]
(if empty?
[:div {:class (stl/css :nitrate-orgs-empty)}
[:span {:class (stl/css :nitrate-penpot-icon)}
[:> raw-svg* {:id penpot-logo-icon}]]
"Penpot"
[:> button* {:variant "ghost"
:type "button"
:class (stl/css :nitrate-create-org)
:on-click on-create-org-click} (tr "dashboard.create-new-org")]]
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click on-show-teams-click
:on-key-down on-show-teams-keydown}
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click on-show-teams-click
:on-key-down on-show-teams-keydown}
[:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team)
:class (stl/css :team-picture)
:alt (:name team)}]
[:span {:class (stl/css :team-text) :title (:name team)} (:name team)]]
[:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url current-org)
:class (stl/css :team-picture)
:alt (:name current-org)}]
[:span {:class (stl/css :team-text) :title (:name current-org)} (:name current-org)]]
arrow-icon]]
arrow-icon]]
;; Teams Dropdown
;; Teams Dropdown
[:> teams-selector-dropdown* {:show show-teams-menu?
:on-close close-teams-menu
:id "organizations-list"
:class (stl/css :dropdown :teams-dropdown)
:team current-org
:profile profile
:teams orgs
:show-default-team false
:allow-create-teams false
:allow-create-org true}]])))
[:> teams-selector-dropdown* {:show show-teams-menu?
:on-close close-teams-menu
:id "organizations-list"
:class (stl/css :dropdown :teams-dropdown)
:team team
:profile profile
:teams teams
:show-default-team false
:allow-create-teams false
:allow-create-org true}]]))
(mf/defc sidebar-team-switch*
[{:keys [team profile]}]
@@ -628,7 +601,7 @@
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click)))))
(dom/click!)))))
close-team-options-menu
(mf/use-fn #(reset! show-team-options-menu* false))
@@ -648,7 +621,7 @@
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click)))))
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))]
@@ -732,8 +705,6 @@
overflow* (mf/use-state false)
overflow? (deref overflow*)
nitrate? (contains? cf/flags :nitrate)
go-projects
(mf/use-fn #(st/emit! (dcm/go-to-dashboard-recent)))
@@ -822,71 +793,70 @@
(reset! overflow* (> scroll-height client-height))))
[:*
[:div {:ref container}
(when nitrate?
[:div {:class (stl/css :nitrate-orgs-container)}
[:> sidebar-org-switch* {:team team :profile profile}]])
[:div {:class (stl/css-case :sidebar-content true :sidebar-content-nitrate nitrate?)}
[:> sidebar-team-switch* {:team team :profile profile}]
[:div {:class (stl/css-case :sidebar-content true)
:ref container}
(when (contains? cf/flags :nitrate)
[:> sidebar-org-switch* {:team team :profile profile}])
[:> sidebar-team-switch* {:team team :profile profile}]
[:> sidebar-search* {:search-term search-term
:team-id (:id team)}]
[:> sidebar-search* {:search-term search-term
:team-id (:id team)}]
[:div {:class (stl/css :sidebar-content-section)}
[:ul {:class (stl/css :sidebar-nav)}
[:li {:class (stl/css-case :recent-projects true
:sidebar-nav-item true
:current projects?)}
[:& link {:action go-projects
:class (stl/css :sidebar-link)
:keyboard-action go-projects-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
[:div {:class (stl/css :sidebar-content-section)}
[:ul {:class (stl/css :sidebar-nav)}
[:li {:class (stl/css-case :recent-projects true
:sidebar-nav-item true
:current projects?)}
[:& link {:action go-projects
:class (stl/css :sidebar-link)
:keyboard-action go-projects-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.projects")]]]
[:li {:class (stl/css-case :current drafts?
:sidebar-nav-item true)}
[:& link {:action go-drafts
:class (stl/css :sidebar-link)
:keyboard-action go-drafts-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]]]
[:li {:class (stl/css-case :current drafts?
:sidebar-nav-item true)}
[:& link {:action go-drafts
:class (stl/css :sidebar-link)
:keyboard-action go-drafts-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.drafts")]]]]]
[:div {:class (stl/css :sidebar-content-section)}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.sources")]
[:ul {:class (stl/css :sidebar-nav)}
[:li {:class (stl/css-case :sidebar-nav-item true
:current fonts?)}
[:& link {:action go-fonts
:class (stl/css :sidebar-link)
:keyboard-action go-fonts-with-key
:data-testid "fonts"}
[:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]
[:li {:class (stl/css-case :current libs?
:sidebar-nav-item true)}
[:& link {:action go-libs
:data-testid "libs-link-sidebar"
:class (stl/css :sidebar-link)
:keyboard-action go-libs-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]]
[:div {:class (stl/css :sidebar-content-section)}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.sources")]
[:ul {:class (stl/css :sidebar-nav)}
[:li {:class (stl/css-case :sidebar-nav-item true
:current fonts?)}
[:& link {:action go-fonts
:class (stl/css :sidebar-link)
:keyboard-action go-fonts-with-key
:data-testid "fonts"}
[:span {:class (stl/css :element-title)} (tr "labels.fonts")]]]
[:li {:class (stl/css-case :current libs?
:sidebar-nav-item true)}
[:& link {:action go-libs
:data-testid "libs-link-sidebar"
:class (stl/css :sidebar-link)
:keyboard-action go-libs-with-key}
[:span {:class (stl/css :element-title)} (tr "labels.shared-libraries")]]]]]
[:div {:class (stl/css :sidebar-content-section)
:data-testid "pinned-projects"}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.pinned-projects")]
(if (some? pinned-projects)
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
(for [item pinned-projects]
[:> sidebar-project*
{:item item
:key (dm/str (:id item))
:id (:id item)
:team-id (:id team)
:is-selected (= (:id item) (:id project))}])]
[:div {:class (stl/css :sidebar-empty-placeholder)}
pin-icon
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]]))
[:div {:class (stl/css :sidebar-content-section)
:data-testid "pinned-projects"}
[:div {:class (stl/css :sidebar-section-title)}
(tr "labels.pinned-projects")]
(if (some? pinned-projects)
[:ul {:class (stl/css :sidebar-nav :pinned-projects)}
(for [item pinned-projects]
[:> sidebar-project*
{:item item
:key (dm/str (:id item))
:id (:id item)
:team-id (:id team)
:is-selected (= (:id item) (:id project))}])]
[:div {:class (stl/css :sidebar-empty-placeholder)}
pin-icon
[:span {:class (stl/css :empty-text)} (tr "dashboard.no-projects-placeholder")]])]]
[:div {:class (stl/css-case :separator true :overflow-separator overflow?)}]]))
(mf/defc help-learning-menu*
{::mf/props :obj
@@ -1086,13 +1056,10 @@
(dom/open-new-window "https://penpot.app/pricing")))]
[:*
(if (contains? cf/flags :nitrate)
(when-not (:nitrate-licence profile)
[:> nitrate-sidebar* {:profile profile}])
(when (contains? cf/flags :subscriptions)
(if (show-subscription-dashboard-banner? profile)
[:> dashboard-cta* {:profile profile}]
[:> subscription-sidebar* {:profile profile}])))
(when (contains? cf/flags :subscriptions)
(if (show-subscription-dashboard-banner? profile)
[:> dashboard-cta* {:profile profile}]
[:> subscription-sidebar* {:profile profile}]))
;; TODO remove this block when subscriptions is full implemented
(when (contains? cf/flags :subscriptions-old)

View File

@@ -40,11 +40,6 @@
overflow-y: auto;
}
.sidebar-content-nitrate {
padding: var(--sp-m) 0 0 0;
border-block-start: $b-1 solid var(--color-background-quaternary);
}
.separator {
height: var(--sp-xxs);
width: 94%;
@@ -519,44 +514,3 @@
@include t.use-typography("body-small");
color: var(--color-accent-tertiary);
}
.nitrate-orgs-container {
align-items: center;
display: flex;
height: calc(2 * var(--sp-xxxl));
max-height: calc(2 * var(--sp-xxxl));
justify-content: space-between;
padding: var(--sp-xs) var(--sp-l) var(--sp-xs) var(--sp-s);
// border-block-end: $b-1 solid var(--color-background-quaternary);
}
.nitrate-orgs-empty {
@include t.use-typography("body-medium");
color: var(--color-foreground-primary);
width: 100%;
margin: var(--sp-xs) var(--sp-l);
display: flex;
align-items: center;
gap: var(--sp-s);
}
.nitrate-create-org {
margin-inline-start: auto;
text-transform: uppercase;
}
.nitrate-penpot-icon {
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
height: var(--sp-xxxl);
width: var(--sp-xxxl);
background-color: var(--color-foreground-primary);
svg {
fill: var(--icon-stroke-color);
width: var(--sp-xxl);
height: var(--sp-xxl);
}
}

View File

@@ -6,7 +6,6 @@
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.router :as rt]
[app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
@@ -116,26 +115,6 @@
:has-dropdown false
:is-highlighted false}]))))
(mf/defc nitrate-sidebar*
[]
(let [handle-click
(mf/use-fn
(fn []
(st/emit! (modal/show :nitrate-form {}))))]
;; TODO add translations for this texts when we have the definitive ones
[:div {:class (stl/css :nitrate-banner :highlighted)}
[:div {:class (stl/css :nitrate-content)}
[:span {:class (stl/css :nitrate-title)} "Unlock Nitrate features"]]
[:div {:class (stl/css :nitrate-content)}
[:span {:class (stl/css :nitrate-info)} "Some further information and explanation."]
[:> button* {:variant "primary"
:type "button"
:class (stl/css :cta-bottom-button)
:on-click handle-click} "UPGRADE TO NITRATE"]]]))
(mf/defc team*
[{:keys [is-owner team]}]
(let [subscription (:subscription team)

View File

@@ -205,28 +205,3 @@
overflow-wrap: break-word;
}
}
.nitrate-banner {
display: flex;
border-radius: var(--sp-s);
flex-direction: column;
margin: var(--sp-m);
background: var(--color-background-quaternary);
border: $b-1 solid var(--color-accent-primary-muted);
padding: var(--sp-l);
}
.nitrate-title {
@include t.use-typography("body-large");
color: var(--color-foreground-primary);
}
.nitrate-info {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
}
.nitrate-content {
display: flex;
flex-direction: column;
}

View File

@@ -18,7 +18,6 @@ $sz-32: px2rem(32);
$sz-36: px2rem(36);
$sz-40: px2rem(40);
$sz-48: px2rem(48);
$sz-64: px2rem(64);
$sz-88: px2rem(88);
$sz-96: px2rem(96);
$sz-120: px2rem(120);

View File

@@ -8,6 +8,7 @@
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.constants :refer [max-input-length]]
[app.main.ui.ds.controls.utilities.hint-message :refer [hint-message*]]
[app.main.ui.ds.controls.utilities.input-field :refer [input-field*]]
@@ -51,11 +52,10 @@
:has-hint has-hint
:hint-type hint-type
:variant variant})]
[:div {:class [class (stl/css-case :input-wrapper true
:variant-dense (= variant "dense")
:variant-comfortable (= variant "comfortable")
:has-hint has-hint)]}
[:div {:class (dm/str class " " (stl/css-case :input-wrapper true
:variant-dense (= variant "dense")
:variant-comfortable (= variant "comfortable")
:has-hint has-hint))}
(when has-label
[:> label* {:for id :is-optional is-optional} label])
[:> input-field* props]
@@ -64,3 +64,4 @@
:class hint-class
:message hint-message
:type hint-type}])]))

View File

@@ -84,7 +84,6 @@
:on-click on-icon-click}])
(if aria-label
[:> tooltip* {:content aria-label
:class (stl/css :tooltip-wrapper)
:id tooltip-id}
[:> "input" props]]
[:> "input" props])

View File

@@ -120,7 +120,3 @@
color: var(--color-foreground-secondary);
min-inline-size: var(--sp-l);
}
.tooltip-wrapper {
inline-size: 100%;
}

View File

@@ -8,7 +8,6 @@
(:require
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.keyboard :as k]
@@ -48,23 +47,6 @@
[:> input* props]))
(mf/defc form-select*
[{:keys [name] :as props}]
(let [select-name name
form (mf/use-ctx context)
value (get-in @form [:data select-name] "")
handle-change
(fn [event]
(let [value (if (string? event) event (dom/get-target-val event))]
(fm/on-input-change form select-name value)))
props
(mf/spread-props props {:on-change handle-change
:value value})]
[:> select* props]))
(mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}]
(let [form (mf/use-ctx context)
@@ -97,4 +79,4 @@
(when (fn? on-submit)
(on-submit form event))))]
[:> (mf/provider context) {:value form}
[:form {:class class :on-submit on-submit'} children]]))
[:form {:class class :on-submit on-submit'} children]]))

View File

@@ -36,7 +36,7 @@
["/feedback" :settings-feedback]
["/options" :settings-options]
["/subscriptions" :settings-subscription]
["/integrations" :settings-integrations]
["/access-tokens" :settings-access-tokens]
["/notifications" :settings-notifications]]
["/frame-preview" :frame-preview]

View File

@@ -13,10 +13,10 @@
[app.main.store :as st]
[app.main.ui.hooks :as hooks]
[app.main.ui.modal :refer [modal-container*]]
[app.main.ui.settings.access-tokens :refer [access-tokens-page]]
[app.main.ui.settings.change-email]
[app.main.ui.settings.delete-account]
[app.main.ui.settings.feedback :refer [feedback-page*]]
[app.main.ui.settings.integrations :refer [integrations-page*]]
[app.main.ui.settings.notifications :refer [notifications-page*]]
[app.main.ui.settings.options :refer [options-page]]
[app.main.ui.settings.password :refer [password-page]]
@@ -73,8 +73,8 @@
:settings-subscription
[:> subscription-page* {:profile profile}]
:settings-integrations
[:> integrations-page*]
:settings-access-tokens
[:& access-tokens-page]
:settings-notifications
[:& notifications-page* {:profile profile}])]]]]))

View File

@@ -0,0 +1,291 @@
;; 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.settings.access-tokens
(:require-macros [app.main.style :as stl])
(:require
[app.common.schema :as sm]
[app.common.time :as ct]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.profile :as du]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.components.forms :as fm]
[app.main.ui.icons :as deprecated-icon]
[app.util.clipboard :as clipboard]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private clipboard-icon
(deprecated-icon/icon-xref :clipboard (stl/css :clipboard-icon)))
(def ^:private close-icon
(deprecated-icon/icon-xref :close (stl/css :close-icon)))
(def ^:private menu-icon
(deprecated-icon/icon-xref :menu (stl/css :menu-icon)))
(def tokens-ref
(l/derived :access-tokens st/state))
(def token-created-ref
(l/derived :access-token-created st/state))
(def ^:private schema:form
[:map {:title "AccessTokenForm"}
[:name [::sm/text {:max 250}]]
[:expiration-date [::sm/text {:max 250}]]])
(def initial-data
{:name "" :expiration-date "never"})
(mf/defc access-token-modal
{::mf/register modal/components
::mf/register-as :access-token}
[]
(let [form (fm/use-form
:initial initial-data
:schema schema:form)
created (mf/deref token-created-ref)
created? (mf/use-state false)
on-success
(mf/use-fn
(mf/deps created)
(fn [_]
(let [message (tr "dashboard.access-tokens.create.success")]
(st/emit! (du/fetch-access-tokens)
(ntf/success message)
(reset! created? true)))))
on-close
(mf/use-fn
(mf/deps created)
(fn [_]
(reset! created? false)
(st/emit! (modal/hide))))
on-error
(mf/use-fn
(fn [_]
(st/emit! (ntf/error (tr "errors.generic"))
(modal/hide))))
on-submit
(mf/use-fn
(fn [form]
(let [cdata (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
expiration (:expiration-date cdata)
params (cond-> {:name (:name cdata)
:perms (:perms cdata)}
(not= "never" expiration) (assoc :expiration expiration))]
(st/emit! (du/create-access-token
(with-meta params mdata))))))
copy-token
(mf/use-fn
(mf/deps created)
(fn [event]
(dom/prevent-default event)
(clipboard/to-clipboard (:token created))
(st/emit! (ntf/show {:level :info
:type :toast
:content (tr "dashboard.access-tokens.copied-success")
:timeout 7000}))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:& fm/form {:form form :on-submit on-submit}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} (tr "modals.create-access-token.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click on-close}
close-icon]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :fields-row)}
[:& fm/input {:type "text"
:auto-focus? true
:form form
:name :name
:disabled @created?
:label (tr "modals.create-access-token.name.label")
:show-success? true
:placeholder (tr "modals.create-access-token.name.placeholder")}]]
[:div {:class (stl/css :fields-row)}
[:div {:class (stl/css :select-title)}
(tr "modals.create-access-token.expiration-date.label")]
[:& fm/select {:options [{:label (tr "dashboard.access-tokens.expiration-never") :value "never" :key "never"}
{:label (tr "dashboard.access-tokens.expiration-30-days") :value "720h" :key "720h"}
{:label (tr "dashboard.access-tokens.expiration-60-days") :value "1440h" :key "1440h"}
{:label (tr "dashboard.access-tokens.expiration-90-days") :value "2160h" :key "2160h"}
{:label (tr "dashboard.access-tokens.expiration-180-days") :value "4320h" :key "4320h"}]
:default "never"
:disabled @created?
:name :expiration-date}]
(when @created?
[:span {:class (stl/css :token-created-info)}
(if (:expires-at created)
(tr "dashboard.access-tokens.token-will-expire" (ct/format-inst (:expires-at created) "PPP"))
(tr "dashboard.access-tokens.token-will-not-expire"))])]
[:div {:class (stl/css :fields-row)}
(when @created?
[:div {:class (stl/css :custon-input-wrapper)}
[:input {:type "text"
:value (:token created "")
:class (stl/css :custom-input-token)
:read-only true}]
[:button {:title (tr "modals.create-access-token.copy-token")
:class (stl/css :copy-btn)
:on-click copy-token}
clipboard-icon]])
#_(when @created?
[:button {:class (stl/css :copy-btn)
:title (tr "modals.create-access-token.copy-token")
:on-click copy-token}
[:span {:class (stl/css :token-value)} (:token created "")]
[:span {:class (stl/css :icon)}
i/clipboard]])]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
(if @created?
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.close")
:on-click modal/hide!}]
[:*
[:input {:class (stl/css :cancel-button)
:type "button"
:value (tr "labels.cancel")
:on-click modal/hide!}]
[:> fm/submit-button*
{:large? false :label (tr "modals.create-access-token.submit-label")}]])]]]]]))
(mf/defc access-tokens-hero
[]
(let [on-click (mf/use-fn #(st/emit! (modal/show :access-token {})))]
[:div {:class (stl/css :access-tokens-hero)}
[:h2 {:class (stl/css :hero-title)} (tr "dashboard.access-tokens.personal")]
[:p {:class (stl/css :hero-desc)} (tr "dashboard.access-tokens.personal.description")]
[:button {:class (stl/css :hero-btn)
:on-click on-click}
(tr "dashboard.access-tokens.create")]]))
(mf/defc access-token-actions
[{:keys [on-delete]}]
(let [local (mf/use-state {:menu-open false})
show? (:menu-open @local)
options (mf/with-memo [on-delete]
[{:name (tr "labels.delete")
:id "access-token-delete"
:handler on-delete}])
menu-ref (mf/use-ref)
on-menu-close
(mf/use-fn #(swap! local assoc :menu-open false))
on-menu-click
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(swap! local assoc :menu-open true)))
on-keydown
(mf/use-fn
(mf/deps on-menu-click)
(fn [event]
(when (kbd/enter? event)
(dom/stop-propagation event)
(on-menu-click event))))]
[:button {:class (stl/css :menu-btn)
:tab-index "0"
:ref menu-ref
:on-click on-menu-click
:on-key-down on-keydown}
menu-icon
[:> context-menu*
{:on-close on-menu-close
:show show?
:fixed true
:min-width true
:top "auto"
:left "auto"
:options options}]]))
(mf/defc access-token-item
{::mf/wrap [mf/memo]}
[{:keys [token] :as props}]
(let [expires-at (:expires-at token)
expires-txt (some-> expires-at (ct/format-inst "PPP"))
expired? (and (some? expires-at) (> (ct/now) expires-at))
delete-fn
(mf/use-fn
(mf/deps token)
(fn []
(let [params {:id (:id token)}
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
(st/emit! (du/delete-access-token (with-meta params mdata))))))
on-delete
(mf/use-fn
(mf/deps delete-fn)
(fn []
(st/emit! (modal/show
{:type :confirm
:title (tr "modals.delete-acces-token.title")
:message (tr "modals.delete-acces-token.message")
:accept-label (tr "modals.delete-acces-token.accept")
:on-accept delete-fn}))))]
[:div {:class (stl/css :table-row)}
[:div {:class (stl/css :table-field :field-name)}
(str (:name token))]
[:div {:class (stl/css-case :expiration-date true
:expired expired?)}
(cond
(nil? expires-at) (tr "dashboard.access-tokens.no-expiration")
expired? (tr "dashboard.access-tokens.expired-on" expires-txt)
:else (tr "dashboard.access-tokens.expires-on" expires-txt))]
[:div {:class (stl/css :table-field :actions)}
[:& access-token-actions
{:on-delete on-delete}]]]))
(mf/defc access-tokens-page
[]
(let [tokens (mf/deref tokens-ref)]
(mf/with-effect []
(dom/set-html-title (tr "title.settings.access-tokens"))
(st/emit! (du/fetch-access-tokens)))
[:div {:class (stl/css :dashboard-access-tokens)}
[:& access-tokens-hero]
(if (empty? tokens)
[:div {:class (stl/css :access-tokens-empty)}
[:div (tr "dashboard.access-tokens.empty.no-access-tokens")]
[:div (tr "dashboard.access-tokens.empty.add-one")]]
[:div {:class (stl/css :dashboard-table)}
[:div {:class (stl/css :table-rows)}
(for [token tokens]
[:& access-token-item {:token token :key (:id token)}])]])]))

View File

@@ -0,0 +1,202 @@
// 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;
// ACCESS TOKENS PAGE
.dashboard-access-tokens {
display: grid;
grid-template-rows: auto 1fr;
margin: deprecated.$s-80 auto deprecated.$s-120 auto;
gap: deprecated.$s-32;
width: deprecated.$s-800;
}
// hero
.access-tokens-hero {
display: grid;
grid-template-rows: auto auto 1fr;
gap: deprecated.$s-32;
width: deprecated.$s-500;
font-size: deprecated.$fs-14;
margin: deprecated.$s-16 auto 0 auto;
}
.hero-title {
@include deprecated.bigTitleTipography;
color: var(--title-foreground-color-hover);
}
.hero-desc {
color: var(--title-foreground-color);
margin-bottom: 0;
font-size: deprecated.$fs-14;
}
.hero-btn {
@extend .button-primary;
}
// table empty
.access-tokens-empty {
display: grid;
place-items: center;
align-content: center;
height: deprecated.$s-156;
max-width: deprecated.$s-1000;
width: 100%;
padding: deprecated.$s-32;
border: deprecated.$s-1 solid var(--panel-border-color);
border-radius: deprecated.$br-8;
color: var(--dashboard-list-text-foreground-color);
}
// Access tokens table
.dashboard-table {
height: fit-content;
}
.table-rows {
display: grid;
grid-auto-rows: deprecated.$s-64;
gap: deprecated.$s-16;
width: 100%;
height: 100%;
max-width: deprecated.$s-1000;
margin-top: deprecated.$s-16;
color: var(--title-foreground-color);
}
.table-row {
display: grid;
grid-template-columns: 43% 1fr auto;
align-items: center;
height: deprecated.$s-64;
width: 100%;
padding: 0 deprecated.$s-16;
border-radius: deprecated.$br-8;
background-color: var(--dashboard-list-background-color);
color: var(--dashboard-list-foreground-color);
}
.field-name {
@include deprecated.textEllipsis;
display: grid;
width: 43%;
min-width: deprecated.$s-300;
}
.expiration-date {
@include deprecated.flexCenter;
min-width: deprecated.$s-76;
width: fit-content;
height: deprecated.$s-24;
border-radius: deprecated.$br-8;
color: var(--dashboard-list-text-foreground-color);
}
.expired {
@include deprecated.headlineSmallTypography;
padding: 0 deprecated.$s-6;
color: var(--pill-foreground-color);
background-color: var(--status-widget-background-color-warning);
}
.actions {
position: relative;
}
.menu-icon {
@extend .button-icon;
stroke: var(--icon-foreground);
}
.menu-btn {
@include deprecated.buttonStyle;
}
// Create access token modal
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
@extend .modal-container-base;
min-width: deprecated.$s-408;
}
.modal-header {
margin-bottom: deprecated.$s-24;
}
.modal-title {
@include deprecated.uppercaseTitleTipography;
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
@extend .modal-close-btn-base;
}
.modal-content {
@include deprecated.flexColumn;
gap: deprecated.$s-24;
@include deprecated.bodySmallTypography;
margin-bottom: deprecated.$s-24;
}
.select-title {
@include deprecated.bodySmallTypography;
color: var(--modal-title-foreground-color);
}
.custon-input-wrapper {
@include deprecated.flexRow;
border-radius: deprecated.$br-8;
height: deprecated.$s-32;
background-color: var(--input-background-color);
}
.custom-input-token {
@extend .input-element;
@include deprecated.bodySmallTypography;
margin: 0;
flex-grow: 1;
&:focus {
outline: none;
border: deprecated.$s-1 solid var(--input-border-color-active);
}
}
.token-value {
@include deprecated.textEllipsis;
@include deprecated.bodySmallTypography;
flex-grow: 1;
}
.copy-btn {
@include deprecated.flexCenter;
@extend .button-secondary;
height: deprecated.$s-28;
width: deprecated.$s-28;
}
.clipboard-icon {
@extend .button-icon-small;
}
.token-created-info {
color: var(--modal-text-foreground-color);
}
.action-buttons {
@extend .modal-action-btns;
button {
@extend .modal-accept-btn;
}
}
.cancel-button {
@extend .modal-cancel-btn;
}

View File

@@ -1,573 +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.settings.integrations
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.schema :as sm]
[app.common.time :as ct]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.data.profile :as du]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.switch :refer [switch*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.notifications.shared.notification-pill :refer [notification-pill*]]
[app.main.ui.ds.tooltip :refer [tooltip*]]
[app.main.ui.forms :as fc]
[app.util.clipboard :as clipboard]
[app.util.dom :as dom]
[app.util.forms :as fm]
[app.util.i18n :as i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def tokens-ref
(l/derived :access-tokens st/state))
(def token-created-ref
(l/derived :access-token-created st/state))
(def notification-timeout 7000)
(def ^:private schema:form
[:map
[:name [::sm/text {:max 250}]]
[:expiration-date [::sm/text {:max 250}]]])
(def form-initial-data
{:name ""
:expiration-date "never"})
(mf/defc token-created*
{::mf/private true}
[{:keys [title]}]
(let [token-created (mf/deref token-created-ref)
on-copy-to-clipboard
(mf/use-fn
(mf/deps token-created)
(fn [event]
(dom/prevent-default event)
(clipboard/to-clipboard (:token token-created))
(st/emit! (ntf/show {:level :info
:type :toast
:content (tr "integrations.notification.success.copied")
:timeout notification-timeout}))))]
[:div {:class (stl/css :modal-form)}
[:> text* {:as "h2"
:typography t/headline-large
:class (stl/css :color-primary)}
title]
[:> notification-pill* {:level :info
:type :context}
(tr "integrations.info.non-recuperable")]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-token)}
[:> input* {:type "text"
:default-value (:token token-created "")
:read-only true}]
[:div {:class (stl/css :modal-token-button)}
[:> icon-button* {:variant "secondary"
:aria-label (tr "integrations.copy-token")
:on-click on-copy-to-clipboard
:icon i/clipboard}]]]
[:> text* {:as "div"
:typography t/body-small
:class (stl/css :color-secondary)}
(if (:expires-at token-created)
(tr "integrations.token-will-expire" (ct/format-inst (:expires-at token-created) "PPP"))
(tr "integrations.token-will-not-expire"))]]
[:div {:class (stl/css :modal-footer)}
[:> button* {:variant "secondary"
:on-click modal/hide!}
(tr "labels.close")]]]))
(mf/defc create-token*
{::mf/private true}
[{:keys [title info mcp-key? on-created]}]
(let [form (fm/use-form
:initial form-initial-data
:schema schema:form)
on-error
(mf/use-fn
#(st/emit! (ntf/error (tr "errors.generic"))
(modal/hide)))
on-success
(mf/use-fn
#(st/emit! (du/fetch-access-tokens)
(ntf/success (tr "integrations.notification.success.created"))
(on-created)))
on-submit
(mf/use-fn
(fn [form]
(let [cdata (:clean-data @form)
mdata {:on-success (partial on-success form)
:on-error (partial on-error form)}
expiration (:expiration-date cdata)
params (cond-> {:name (:name cdata)
:perms (:perms cdata)}
(not= "never" expiration) (assoc :expiration expiration)
(true? mcp-key?) (assoc :type "mcp"))]
(st/emit! (du/create-access-token (with-meta params mdata))))))]
[:> fc/form* {:form form
:class (stl/css :modal-form)
:on-submit on-submit}
[:> text* {:as "h2"
:typography t/headline-large
:class (stl/css :color-primary)}
title]
(when (some? info)
[:> notification-pill* {:level :info
:type :context}
info])
[:div {:class (stl/css :modal-content)}
[:> fc/form-input* {:type "text"
:auto-focus? true
:form form
:name :name
:label (tr "integrations.name.label")
:placeholder (tr "integrations.name.placeholder")}]]
[:div {:class (stl/css :modal-content)}
[:> text* {:as "label"
:typography t/body-small
:for :expiration-date
:class (stl/css :color-primary)}
(tr "integrations.expiration-date.label")]
[:> fc/form-select* {:options [{:label (tr "integrations.expiration-never") :value "never" :id "never"}
{:label (tr "integrations.expiration-30-days") :value "720h" :id "720h"}
{:label (tr "integrations.expiration-60-days") :value "1440h" :id "1440h"}
{:label (tr "integrations.expiration-90-days") :value "2160h" :id "2160h"}
{:label (tr "integrations.expiration-180-days") :value "4320h" :id "4320h"}]
:default-selected "never"
:name :expiration-date}]]
[:div {:class (stl/css :modal-footer)}
[:> button* {:variant "secondary"
:on-click modal/hide!}
(tr "labels.cancel")]
[:> fc/form-submit* {:variant "primary"}
title]]]))
(mf/defc create-access-token-modal
{::mf/register modal/components
::mf/register-as :create-access-token}
[]
(let [created? (mf/use-state false)
on-close
(mf/use-fn
(fn []
(reset! created? false)
(st/emit! (modal/hide))))
on-created
(mf/use-fn
#(reset! created? true))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-close-button)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}]]
(if @created?
[:> token-created* {:title (tr "integrations.create-access-token.title.created")}]
[:> create-token* {:title (tr "integrations.create-access-token.title")
:on-created on-created}])]]))
(mf/defc create-mcp-key-modal
{::mf/register modal/components
::mf/register-as :create-mcp-key}
[]
(let [created? (mf/use-state false)
on-close
(mf/use-fn
(fn []
(reset! created? false)
(st/emit! (modal/hide))))
on-created
(mf/use-fn
(fn []
(st/emit! (du/update-profile-props {:mcp-status true})
(ev/event {::ev/name "create-mcp-key"
::ev/origin "integrations"})
(ev/event {::ev/name "enable-mcp"
::ev/origin "integrations"
:source "key-creation"}))
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-close-button)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}]]
(if @created?
[:> token-created* {:title (tr "integrations.create-mcp-key.title.created")}]
[:> create-token* {:title (tr "integrations.create-mcp-key.title")
:mcp-key? true
:on-created on-created}])]]))
(mf/defc regenerate-mcp-key-modal
{::mf/register modal/components
::mf/register-as :regenerate-mcp-key}
[]
(let [created? (mf/use-state false)
tokens (mf/deref tokens-ref)
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
mcp-key-id (:id mcp-key)
on-close
(mf/use-fn
(fn []
(reset! created? false)
(st/emit! (modal/hide))))
on-created
(mf/use-fn
(fn []
(st/emit! (du/delete-access-token {:id mcp-key-id})
(du/update-profile-props {:mcp-status true})
(ev/event {::ev/name "regenerate-mcp-key"
::ev/origin "integrations"}))
(reset! created? true)))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-close-button)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}]]
(if @created?
[:> token-created* {:title (tr "integrations.regenerate-mcp-key.title.created")}]
[:> create-token* {:title (tr "integrations.regenerate-mcp-key.title")
:info (tr "integrations.regenerate-mcp-key.info")
:mcp-key? true
:on-created on-created}])]]))
(mf/defc token-item*
{::mf/private true
::mf/wrap [mf/memo]}
[{:keys [name expires-at on-delete]}]
(let [expires-txt (some-> expires-at (ct/format-inst "PPP"))
expired? (and (some? expires-at) (> (ct/now) expires-at))
menu-open* (mf/use-state false)
menu-open? (deref menu-open*)
handle-menu-close
(mf/use-fn
#(reset! menu-open* false))
handle-menu-click
(mf/use-fn
#(reset! menu-open* (not menu-open?)))
handle-open-confirm-modal
(mf/use-fn
(mf/deps on-delete)
(fn []
(st/emit! (modal/show {:type :confirm
:title (tr "integrations.delete-token.title")
:message (tr "integrations.delete-token.message")
:accept-label (tr "integrations.delete-token.accept")
:on-accept on-delete}))))
options
(mf/with-memo [on-delete]
[{:name (tr "labels.delete")
:id "token-delete"
:handler handle-open-confirm-modal}])]
[:div {:class (stl/css :item)}
[:> text* {:as "div"
:typography t/body-medium
:title name
:class (stl/css :item-title)}
name]
[:> text* {:as "div"
:typography t/body-small
:class (stl/css-case :item-subtitle true
:warning expired?)}
(cond
(nil? expires-at) (tr "integrations.no-expiration")
expired? (tr "integrations.expired-on" expires-txt)
:else (tr "integrations.expires-on" expires-txt))]
[:div {:class (stl/css :item-actions)}
[:> icon-button* {:variant "ghost"
:class (stl/css :item-button)
:aria-pressed menu-open?
:aria-label (tr "labels.options")
:on-click handle-menu-click
:icon i/menu}]
[:> context-menu* {:on-close handle-menu-close
:show menu-open?
:min-width true
:top -10
:left -138
:options options}]]]))
(mf/defc mcp-server-section*
{::mf/private true}
[]
(let [tokens (mf/deref tokens-ref)
profile (mf/deref refs/profile)
mcp-key (some #(when (= (:type %) "mcp") %) tokens)
mcp-active? (d/nilv (-> profile :props :mcp-status) false)
expires-at (:expires-at mcp-key)
expired? (and (some? expires-at) (> (ct/now) expires-at))
tooltip-id
(mf/use-id)
handle-mcp-status-change
(mf/use-fn
(fn [mcp-status]
(st/emit! (du/update-profile-props {:mcp-status mcp-status})
(ntf/show {:level :info
:type :toast
:content (if (true? mcp-status)
(tr "integrations.notification.success.mcp-server-enabled")
(tr "integrations.notification.success.mcp-server-disabled"))
:timeout notification-timeout})
(ev/event {::ev/name (if (true? mcp-status) "enable-mcp" "disable-mcp")
::ev/origin "integrations"
:source "toggle"}))))
handle-initial-mcp-status
(mf/use-fn
#(st/emit! (modal/show {:type :create-mcp-key})))
handle-regenerate-mcp-key
(mf/use-fn
#(st/emit! (modal/show {:type :regenerate-mcp-key})))
handle-delete
(mf/use-fn
(mf/deps mcp-key)
(fn []
(let [params {:id (:id mcp-key)}
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
(st/emit! (du/delete-access-token (with-meta params mdata))
(du/update-profile-props {:mcp-status false})))))
on-copy-to-clipboard
(mf/use-fn
(fn [event]
(dom/prevent-default event)
(clipboard/to-clipboard cf/mcp-server-url)
(st/emit! (ntf/show {:level :info
:type :toast
:content (tr "integrations.notification.success.copied-link")
:timeout notification-timeout})
(ev/event {::ev/name "copy-mcp-url"
::ev/origin "integrations"}))))]
[:section {:class (stl/css :mcp-server-section)}
[:div
[:div {:class (stl/css :title)}
[:> heading* {:level 2
:typography t/title-medium
:class (stl/css :color-primary :mcp-server-title)}
(tr "integrations.mcp-server.title")]
[:> text* {:as "span"
:typography t/body-small
:class (stl/css :beta)}
(tr "integrations.mcp-server.title.beta")]]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-secondary)}
(tr "integrations.mcp-server.description")]]
[:div
[:> text* {:as "h3"
:typography t/headline-small
:class (stl/css :color-primary)}
(tr "integrations.mcp-server.status")]
[:div {:class (stl/css :mcp-server-block)}
(when expired?
[:> notification-pill* {:level :error
:type :context}
[:div {:class (stl/css :mcp-server-notification)}
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-primary)}
(tr "integrations.mcp-server.status.expired.0")]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-primary)}
(tr "integrations.mcp-server.status.expired.1")]]])
[:div {:class (stl/css :mcp-server-switch)}
[:> switch* {:label (if mcp-active?
(tr "integrations.mcp-server.status.enabled")
(tr "integrations.mcp-server.status.disabled"))
:default-checked mcp-active?
:on-change handle-mcp-status-change}]
(when (and (false? mcp-active?) (nil? mcp-key))
[:div {:class (stl/css :mcp-server-switch-cover)
:on-click handle-initial-mcp-status}])]]]
(when (some? mcp-key)
[:div {:class (stl/css :mcp-server-key)}
[:> text* {:as "h3"
:typography t/headline-small
:class (stl/css :color-primary)}
(tr "integrations.mcp-server.mcp-keys.title")]
[:div {:class (stl/css :mcp-server-block)}
[:div {:class (stl/css :mcp-server-regenerate)}
[:> button* {:variant "primary"
:class (stl/css :fit-content)
:on-click handle-regenerate-mcp-key}
(tr "integrations.mcp-server.mcp-keys.regenerate")]
[:> tooltip* {:content (tr "integrations.mcp-server.mcp-keys.tootip")
:id tooltip-id}
[:> icon* {:icon-id i/info
:class (stl/css :color-secondary)}]]]
[:div {:class (stl/css :list)}
[:> token-item* {:key (:id mcp-key)
:name (:name mcp-key)
:expires-at (:expires-at mcp-key)
:on-delete handle-delete}]]]])
[:> notification-pill* {:level :default
:type :context}
[:div {:class (stl/css :mcp-server-notification)}
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-secondary)}
(tr "integrations.mcp-server.mcp-keys.info")]
[:div {:class (stl/css :mcp-server-notification-line)}
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-primary)}
cf/mcp-server-url]
[:> text* {:as "div"
:typography t/body-medium
:on-click on-copy-to-clipboard
:class (stl/css :mcp-server-notification-link)}
[:> icon* {:icon-id i/clipboard}] (tr "integrations.mcp-server.mcp-keys.copy")]]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-secondary)}
[:a {:href cf/mcp-help-center-uri
:class (stl/css :mcp-server-notification-link)}
(tr "integrations.mcp-server.mcp-keys.help") [:> icon* {:icon-id i/open-link}]]]]]]))
(mf/defc access-tokens-section*
{::mf/private true}
[]
(let [tokens (mf/deref tokens-ref)
handle-click
(mf/use-fn
#(st/emit! (modal/show {:type :create-access-token})))
handle-delete
(mf/use-fn
(fn [token-id]
(let [params {:id token-id}
mdata {:on-success #(st/emit! (du/fetch-access-tokens))}]
(st/emit! (du/delete-access-token (with-meta params mdata))))))]
[:section {:class (stl/css :access-tokens-section)}
[:> heading* {:level 2
:typography t/title-medium
:class (stl/css :color-primary)}
(tr "integrations.access-tokens.personal")]
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-secondary)}
(tr "integrations.access-tokens.personal.description")]
[:> button* {:variant "primary"
:class (stl/css :fit-content)
:on-click handle-click}
(tr "integrations.access-tokens.create")]
(if (empty? tokens)
[:div {:class (stl/css :frame)}
[:> text* {:as "div"
:typography t/body-medium
:class (stl/css :color-secondary :text-center)}
[:div (tr "integrations.access-tokens.empty.no-access-tokens")]
[:div (tr "integrations.access-tokens.empty.add-one")]]]
[:div {:class (stl/css :list)}
(for [token tokens]
(when (nil? (:type token))
[:> token-item* {:key (:id token)
:name (:name token)
:expires-at (:expires-at token)
:on-delete (partial handle-delete (:id token))}]))])]))
(mf/defc integrations-page*
[]
(mf/with-effect []
(dom/set-html-title (tr "title.settings.integrations"))
(st/emit! (du/fetch-access-tokens)))
[:div {:class (stl/css :integrations)}
[:> heading* {:level 1
:typography t/title-large
:class (stl/css :color-primary)}
(tr "integrations.title")]
(when (contains? cf/flags :mcp)
[:> mcp-server-section*])
(when (and (contains? cf/flags :mcp)
(contains? cf/flags :access-tokens))
[:hr {:class (stl/css :separator)}])
(when (contains? cf/flags :access-tokens)
[:> access-tokens-section*])])

View File

@@ -1,221 +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;
@use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/mixins.scss" as *;
.color-primary {
color: var(--color-foreground-primary);
}
.color-secondary {
color: var(--color-foreground-secondary);
}
.text-center {
text-align: center;
}
.fit-content {
inline-size: fit-content;
}
.beta {
color: var(--color-accent-primary);
border: $b-1 solid var(--color-accent-primary);
inline-size: fit-content;
padding: var(--sp-xxs) var(--sp-s);
border-radius: $br-4;
}
.title {
display: flex;
flex-direction: row;
align-items: baseline;
gap: var(--sp-s);
}
.modal-overlay {
@extend .modal-overlay-base;
}
.modal-container {
@extend .modal-container-base;
inline-size: $sz-400;
position: relative;
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-xs);
}
.modal-form {
display: flex;
flex-direction: column;
gap: var(--sp-xxxl);
}
.modal-close-button {
position: absolute;
top: var(--sp-s);
right: var(--sp-s);
}
.modal-footer {
display: flex;
justify-content: right;
gap: var(--sp-s);
}
.modal-token {
position: relative;
}
.modal-token-button {
position: absolute;
top: 0;
right: 0;
border-start-start-radius: 0;
border-end-start-radius: 0;
}
.integrations {
display: grid;
grid-template-rows: auto 1fr;
margin: $sz-88 auto $sz-120 auto;
gap: $sz-32;
inline-size: $sz-500;
}
.access-tokens-section {
display: grid;
grid-template-rows: auto auto 1fr;
gap: var(--sp-m);
}
.mcp-server-section {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.mcp-server-key {
display: flex;
flex-direction: column;
}
.mcp-server-notification {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.mcp-server-notification-line {
display: flex;
flex-direction: row;
align-items: center;
gap: var(--sp-m);
}
.mcp-server-notification-link {
cursor: pointer;
color: var(--color-accent-primary);
display: flex;
flex-direction: row;
align-items: center;
gap: var(--sp-xs);
}
.mcp-server-title {
margin: var(--sp-s) 0;
}
.mcp-server-block {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.mcp-server-regenerate {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.mcp-server-switch {
position: relative;
}
.mcp-server-switch-cover {
position: absolute;
inset-block: 0;
inset-inline: 0;
}
.separator {
border: $b-1 solid var(--color-background-quaternary);
margin: var(--sp-s) 0;
}
.frame {
border: $b-1 solid var(--color-background-quaternary);
padding: var(--sp-m);
border-radius: $br-8;
}
.list {
display: grid;
grid-auto-rows: $sz-64;
gap: var(--sp-m);
}
.item {
display: grid;
grid-template-columns: 45% 1fr auto;
align-items: center;
background-color: var(--color-background-tertiary);
border-radius: $br-8;
}
.item-title {
@include textEllipsis;
align-content: center;
block-size: $sz-64;
padding: 0 var(--sp-l);
color: var(--color-foreground-primary);
}
.item-subtitle {
align-content: center;
block-size: $sz-64;
color: var(--color-foreground-secondary);
&.warning {
padding: var(--sp-s) var(--sp-m);
block-size: fit-content;
inline-size: fit-content;
color: var(--color-foreground-primary);
background-color: var(--color-background-warning);
border: $b-1 solid var(--color-accent-warning);
border-radius: $br-8;
}
}
.item-actions {
position: relative;
}
.item-button {
block-size: $sz-64;
inline-size: $sz-48;
border-radius: 0 var(--sp-s) var(--sp-s) 0;
}

View File

@@ -43,8 +43,8 @@
(def ^:private go-settings-subscription
#(st/emit! (rt/nav :settings-subscription)))
(def ^:private go-settings-integrations
#(st/emit! (rt/nav :settings-integrations)))
(def ^:private go-settings-access-tokens
#(st/emit! (rt/nav :settings-access-tokens)))
(def ^:private go-settings-notifications
#(st/emit! (rt/nav :settings-notifications)))
@@ -66,7 +66,7 @@
options? (= section :settings-options)
feedback? (= section :settings-feedback)
subscription? (= section :settings-subscription)
integrations? (= section :settings-integrations)
access-tokens? (= section :settings-access-tokens)
notifications? (= section :settings-notifications)
team-id (or (dtm/get-last-team-id)
(:default-team-id profile))
@@ -115,13 +115,12 @@
:data-testid "settings-subscription"}
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
(when (or (contains? cf/flags :access-tokens)
(contains? cf/flags :mcp))
[:li {:class (stl/css-case :current integrations?
(when (contains? cf/flags :access-tokens)
[:li {:class (stl/css-case :current access-tokens?
:settings-item true)
:on-click go-settings-integrations
:data-testid "settings-integrations"}
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]])
:on-click go-settings-access-tokens
:data-testid "settings-access-tokens"}
[:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
[:hr {:class (stl/css :sidebar-separator)}]

View File

@@ -150,9 +150,7 @@
{::mf/props :obj
::mf/private true}
[{:keys [shapes]}]
(let [multiple? (> (count shapes) 1)
do-copy #(st/emit! (dw/copy-selected))
(let [do-copy #(st/emit! (dw/copy-selected))
do-copy-link #(st/emit! (dw/copy-link-to-clipboard))
do-cut #(st/emit! (dw/copy-selected)
@@ -180,9 +178,6 @@
handle-copy-text
(mf/use-callback #(st/emit! (dw/copy-selected-text)))
handle-copy-as-image
(mf/use-callback #(st/emit! (dw/copy-as-image)))
handle-hover-copy-paste
(mf/use-callback
(fn []
@@ -227,11 +222,6 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-svg")
:on-click handle-copy-svg}]
(when (some cfh/frame-shape? shapes)
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-as-image")
:disabled multiple?
:on-click handle-copy-as-image}])
[:> menu-separator* {}]
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-text")
@@ -239,7 +229,7 @@
[:> menu-entry* {:title (tr "workspace.shape.menu.copy-props")
:shortcut (sc/get-tooltip :copy-props)
:disabled multiple?
:disabled (> (count shapes) 1)
:on-click handle-copy-props}]
[:> menu-entry* {:title (tr "workspace.shape.menu.paste-props")
:shortcut (sc/get-tooltip :paste-props)

View File

@@ -230,6 +230,12 @@
(def get-target-scroll (comp get-scroll-position get-target))
(defn click
"Click a node"
[^js node]
(when (some? node)
(.click node)))
(defn get-files
"Extract the files from dom node."
[^js node]
@@ -470,7 +476,7 @@
(when (some? node)
(.focus node)))
(defn click
(defn click!
[^js node]
(when (some? node)
(.click node)))
@@ -742,11 +748,7 @@
(defn trigger-download
[filename blob]
(let [uri (wapi/create-uri blob)]
(try
(trigger-download-uri filename (.-type ^js blob) uri)
(finally
(wapi/revoke-uri uri)))))
(trigger-download-uri filename (.-type ^js blob) (wapi/create-uri blob)))
(defn event
"Create an instance of DOM Event"

View File

@@ -190,11 +190,6 @@
[{:keys [status]}]
(<= 400 status 499))
(defn blob?
[^js v]
(when (some? v)
(instance? js/Blob v)))
(defn as-promise
[observable]
(p/create

View File

@@ -464,13 +464,6 @@
(let [o (get o type-symbol)]
(= o t))))
#?(:cljs
(def Proxy
(app.util.object/class
:name "Proxy"
:extends js/Object
:constructor (constantly nil))))
(defmacro reify
"A domain specific variation of reify that creates anonymous objects
on demand with the ability to assign protocol implementations and
@@ -488,7 +481,7 @@
obj-sym
(gensym "obj-")]
`(let [~obj-sym (new Proxy)
`(let [~obj-sym (cljs.core/js-obj)
~f-sym (fn [] ~type-name)]
(add-properties! ~obj-sym
{:name ~'js/Symbol.toStringTag

View File

@@ -338,6 +338,77 @@ msgstr "You're going to restore %s."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restore file"
#: src/app/main/ui/settings/access_tokens.cljs:103
msgid "dashboard.access-tokens.copied-success"
msgstr "Copied token"
#: src/app/main/ui/settings/access_tokens.cljs:189
msgid "dashboard.access-tokens.create"
msgstr "Generate new token"
#: src/app/main/ui/settings/access_tokens.cljs:64
msgid "dashboard.access-tokens.create.success"
msgstr "Access token created successfully."
#: src/app/main/ui/settings/access_tokens.cljs:286
msgid "dashboard.access-tokens.empty.add-one"
msgstr "Press the button \"Generate new token\" to generate one."
#: src/app/main/ui/settings/access_tokens.cljs:285
msgid "dashboard.access-tokens.empty.no-access-tokens"
msgstr "You have no tokens so far."
#: src/app/main/ui/settings/access_tokens.cljs:135
msgid "dashboard.access-tokens.expiration-180-days"
msgstr "180 days"
#: src/app/main/ui/settings/access_tokens.cljs:132
msgid "dashboard.access-tokens.expiration-30-days"
msgstr "30 days"
#: src/app/main/ui/settings/access_tokens.cljs:133
msgid "dashboard.access-tokens.expiration-60-days"
msgstr "60 days"
#: src/app/main/ui/settings/access_tokens.cljs:134
msgid "dashboard.access-tokens.expiration-90-days"
msgstr "90 days"
#: src/app/main/ui/settings/access_tokens.cljs:131
msgid "dashboard.access-tokens.expiration-never"
msgstr "Never"
#: src/app/main/ui/settings/access_tokens.cljs:268
msgid "dashboard.access-tokens.expired-on"
msgstr "Expired on %s"
#: src/app/main/ui/settings/access_tokens.cljs:269
msgid "dashboard.access-tokens.expires-on"
msgstr "Expires on %s"
#: src/app/main/ui/settings/access_tokens.cljs:267
msgid "dashboard.access-tokens.no-expiration"
msgstr "No expiration date"
#: src/app/main/ui/settings/access_tokens.cljs:184
msgid "dashboard.access-tokens.personal"
msgstr "Personal access tokens"
#: src/app/main/ui/settings/access_tokens.cljs:185
msgid "dashboard.access-tokens.personal.description"
msgstr ""
"Personal access tokens function like an alternative to our login/password "
"authentication system and can be used to allow an application to access the "
"internal Penpot API"
#: src/app/main/ui/settings/access_tokens.cljs:142
msgid "dashboard.access-tokens.token-will-expire"
msgstr "The token will expire on %s"
#: src/app/main/ui/settings/access_tokens.cljs:143
msgid "dashboard.access-tokens.token-will-not-expire"
msgstr "The token has no expiration date"
#: src/app/main/ui/dashboard/placeholder.cljs:41
msgid "dashboard.add-file"
msgstr "Add file"
@@ -360,7 +431,7 @@ msgstr "(copy)"
#: src/app/main/ui/dashboard/sidebar.cljs:347
msgid "dashboard.create-new-org"
msgstr "+ Create org"
msgstr "Create new org"
#: src/app/main/ui/dashboard/sidebar.cljs:340
msgid "dashboard.create-new-team"
@@ -2063,209 +2134,6 @@ msgstr "Resolved value:"
msgid "inspect.tabs.styles.variants-panel"
msgstr "Variant Properties"
#: src/app/main/ui/settings/integrations.cljs:189
msgid "integrations.access-tokens.create"
msgstr "Create new access token"
#: src/app/main/ui/settings/integrations.cljs:286
msgid "integrations.access-tokens.empty.add-one"
msgstr "Press the button \"Create new access token\" to generate one."
#: src/app/main/ui/settings/integrations.cljs:285
msgid "integrations.access-tokens.empty.no-access-tokens"
msgstr "You have no tokens so far."
#: src/app/main/ui/settings/integrations.cljs:184
msgid "integrations.access-tokens.personal"
msgstr "Personal access tokens"
#: src/app/main/ui/settings/integrations.cljs:185
msgid "integrations.access-tokens.personal.description"
msgstr ""
"Personal access tokens function like an alternative to our login/password "
"authentication system and can be used to allow an application to access the "
"internal Penpot API"
#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158
msgid "integrations.copy-token"
msgstr "Copy token"
#: src/app/main/ui/settings/integrations.cljs:432
msgid "integrations.create-access-token.title"
msgstr "Create access token"
#: src/app/main/ui/settings/integrations.cljs:433
msgid "integrations.create-access-token.title.created"
msgstr "Access token created"
#: src/app/main/ui/settings/integrations.cljs:290
msgid "integrations.create-mcp-key.title"
msgstr "Create new MCP key"
#: src/app/main/ui/settings/integrations.cljs:291
msgid "integrations.create-mcp-key.title.created"
msgstr "MCP key created"
#: src/app/main/ui/settings/integrations.cljs:257
msgid "integrations.delete-token.accept"
msgstr "Delete token"
#: src/app/main/ui/settings/integrations.cljs:256
msgid "integrations.delete-token.message"
msgstr "Are you sure you want to delete this token?"
#: src/app/main/ui/settings/integrations.cljs:255
msgid "integrations.delete-token.title"
msgstr "Delete token"
#: src/app/main/ui/settings/integrations.cljs:135
msgid "integrations.expiration-180-days"
msgstr "180 days"
#: src/app/main/ui/settings/integrations.cljs:132
msgid "integrations.expiration-30-days"
msgstr "30 days"
#: src/app/main/ui/settings/integrations.cljs:133
msgid "integrations.expiration-60-days"
msgstr "60 days"
#: src/app/main/ui/settings/integrations.cljs:134
msgid "integrations.expiration-90-days"
msgstr "90 days"
#: src/app/main/ui/settings/integrations.cljs:131
msgid "integrations.expiration-never"
msgstr "Never"
#: src/app/main/ui/settings/integrations.cljs:268
msgid "integrations.expired-on"
msgstr "Expired on %s"
#: src/app/main/ui/settings/integrations.cljs:269
msgid "integrations.expires-on"
msgstr "Expires on %s"
#: src/app/main/ui/settings/integrations.cljs:267
msgid "integrations.no-expiration"
msgstr "No expiration date"
#: src/app/main/ui/settings/integrations.cljs:130
msgid "integrations.expiration-date.label"
msgstr "Expiration date"
#: src/app/main/ui/settings/integrations.cljs:131
msgid "integrations.info.non-recuperable"
msgstr "This unique token is non-recuperable. If you lose it, you will need to create a new one."
#: src/app/main/ui/settings/integrations.cljs:336
msgid "integrations.mcp-server.title"
msgstr "MCP Server"
#: src/app/main/ui/settings/integrations.cljs:336
msgid "integrations.mcp-server.title.beta"
msgstr "Beta"
#: src/app/main/ui/settings/integrations.cljs:347
msgid "integrations.mcp-server.description"
msgstr "The Penpot MCP Server enables MCP clients to interact directly with Penpot design files."
#: src/app/main/ui/settings/integrations.cljs:353
msgid "integrations.mcp-server.status"
msgstr "Status"
#: src/app/main/ui/settings/integrations.cljs:370
msgid "integrations.mcp-server.status.disabled"
msgstr "Disabled"
#: src/app/main/ui/settings/integrations.cljs:370
msgid "integrations.mcp-server.status.enabled"
msgstr "Enabled"
#: src/app/main/ui/settings/integrations.cljs:363
msgid "integrations.mcp-server.status.expired.0"
msgstr "The MCP key used to connect to the MCP server has expired. As a result, the connection cannot be established."
#: src/app/main/ui/settings/integrations.cljs:368
msgid "integrations.mcp-server.status.expired.1"
msgstr "Please regenerate the MCP key and update your client configuration with the new key."
#: src/app/main/ui/settings/integrations.cljs:415
msgid "integrations.mcp-server.mcp-keys.copy"
msgstr "Copy link"
#: src/app/main/ui/settings/integrations.cljs:422
msgid "integrations.mcp-server.mcp-keys.help"
msgstr "How to configure MCP clients"
#: src/app/main/ui/settings/integrations.cljs:405
msgid "integrations.mcp-server.mcp-keys.info"
msgstr "This is the server url you'll need to configure your MCP client in order to connect it to the Penpot MCP server."
#: src/app/main/ui/settings/integrations.cljs:387
msgid "integrations.mcp-server.mcp-keys.regenerate"
msgstr "Regenerate MCP key"
#: src/app/main/ui/settings/integrations.cljs:381
msgid "integrations.mcp-server.mcp-keys.title"
msgstr "MCP key"
#: src/app/main/ui/settings/integrations.cljs:388
msgid "integrations.mcp-server.mcp-keys.tootip"
msgstr "The MCP key is needed for the MCP client set up"
#: src/app/main/ui/settings/integrations.cljs:124
msgid "integrations.name.label"
msgstr "Name"
#: src/app/main/ui/settings/integrations.cljs:126
msgid "integrations.name.placeholder"
msgstr "The name can help to know what's the token for"
#: src/app/main/ui/settings/integrations.cljs:103
msgid "integrations.notification.success.copied"
msgstr "Copied token"
#: src/app/main/ui/settings/integrations.cljs:64
msgid "integrations.notification.success.created"
msgstr "Token created successfully"
#: src/app/main/ui/settings/integrations.cljs:327
msgid "integrations.notification.success.copied-link"
msgstr "Link copied to clipboard"
#: src/app/main/ui/settings/integrations.cljs:293
msgid "integrations.notification.success.mcp-server-disabled"
msgstr "MCP server disabled"
#: src/app/main/ui/settings/integrations.cljs:299
msgid "integrations.notification.success.mcp-server-enabled"
msgstr "MCP server enabled"
#: src/app/main/ui/settings/integrations.cljs:317
msgid "integrations.regenerate-mcp-key.info"
msgstr "Regenerating the key will immediately revoke the current one. Any application using it will stop working."
#: src/app/main/ui/settings/integrations.cljs:317
msgid "integrations.regenerate-mcp-key.title"
msgstr "Regenerate MCP key"
#: src/app/main/ui/settings/integrations.cljs:318
msgid "integrations.regenerate-mcp-key.title.created"
msgstr "MCP key regenerated"
#: src/app/main/ui/settings/integrations.cljs:480
msgid "integrations.title"
msgstr "Integrations"
#: src/app/main/ui/settings/integrations.cljs:142
msgid "integrations.token-will-expire"
msgstr "The token will expire on %s"
#: src/app/main/ui/settings/integrations.cljs:143
msgid "integrations.token-will-not-expire"
msgstr "The token has no expiration date"
#: src/app/main/ui/dashboard/comments.cljs:96
msgid "label.mark-all-as-read"
msgstr "Mark all as read"
@@ -2486,9 +2354,6 @@ msgstr "Discard"
msgid "labels.download"
msgstr "Download %s"
msgid "labels.download-simple"
msgstr "Download"
#: src/app/main/ui/dashboard/file_menu.cljs:30, src/app/main/ui/dashboard/files.cljs:80, src/app/main/ui/dashboard/files.cljs:179, src/app/main/ui/dashboard/projects.cljs:229, src/app/main/ui/dashboard/projects.cljs:233, src/app/main/ui/dashboard/sidebar.cljs:820
msgid "labels.drafts"
msgstr "Drafts"
@@ -2620,10 +2485,6 @@ msgstr "Info"
msgid "labels.installed-fonts"
msgstr "Installed fonts"
#: src/app/main/ui/settings/sidebar.cljs:123
msgid "labels.integrations"
msgstr "Integrations"
#: src/app/main/ui/static.cljs:405
msgid "labels.internal-error.desc-message-first"
msgstr "Something bad happened."
@@ -3283,6 +3144,30 @@ msgstr "Change email"
msgid "modals.change-email.title"
msgstr "Change your email"
#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158
msgid "modals.create-access-token.copy-token"
msgstr "Copy token"
#: src/app/main/ui/settings/access_tokens.cljs:130
msgid "modals.create-access-token.expiration-date.label"
msgstr "Expiration date"
#: src/app/main/ui/settings/access_tokens.cljs:124
msgid "modals.create-access-token.name.label"
msgstr "Name"
#: src/app/main/ui/settings/access_tokens.cljs:126
msgid "modals.create-access-token.name.placeholder"
msgstr "The name can help to know what's the token for"
#: src/app/main/ui/settings/access_tokens.cljs:178
msgid "modals.create-access-token.submit-label"
msgstr "Create token"
#: src/app/main/ui/settings/access_tokens.cljs:111
msgid "modals.create-access-token.title"
msgstr "Generate access token"
#: src/app/main/ui/dashboard/team.cljs:1127
msgid "modals.create-webhook.submit-label"
msgstr "Create webhook"
@@ -3299,6 +3184,18 @@ msgstr "Payload URL"
msgid "modals.create-webhook.url.placeholder"
msgstr "https://example.com/postreceive"
#: src/app/main/ui/settings/access_tokens.cljs:257
msgid "modals.delete-acces-token.accept"
msgstr "Delete token"
#: src/app/main/ui/settings/access_tokens.cljs:256
msgid "modals.delete-acces-token.message"
msgstr "Are you sure you want to delete this token?"
#: src/app/main/ui/settings/access_tokens.cljs:255
msgid "modals.delete-acces-token.title"
msgstr "Delete token"
#: src/app/main/ui/settings/delete_account.cljs:56
msgid "modals.delete-account.cancel"
msgstr "Cancel and keep my account"
@@ -5192,14 +5089,14 @@ msgstr "Shared Libraries - %s - Penpot"
msgid "title.default"
msgstr "Penpot - Design Freedom for Teams"
#: src/app/main/ui/settings/access_tokens.cljs:278
msgid "title.settings.access-tokens"
msgstr "Profile - Access tokens"
#: src/app/main/ui/settings/feedback.cljs:161
msgid "title.settings.feedback"
msgstr "Give feedback - Penpot"
#: src/app/main/ui/settings/integrations.cljs:278
msgid "title.settings.integrations"
msgstr "Integrations - Penpot"
#: src/app/main/ui/settings/notifications.cljs:45
msgid "title.settings.notifications"
msgstr "Notifications - Penpot"
@@ -7700,18 +7597,6 @@ msgstr "Paste"
msgid "workspace.shape.menu.paste-props"
msgstr "Paste properties"
msgid "workspace.shape.menu.copy-as-image"
msgstr "Copy as image"
msgid "workspace.clipboard.copying"
msgstr "Copying image…"
msgid "workspace.clipboard.image-copied"
msgstr "Image copied to the clipboard"
msgid "workspace.clipboard.image-copy-failed"
msgstr "Error copying image"
#: src/app/main/ui/workspace/context_menu.cljs:443
msgid "workspace.shape.menu.path"
msgstr "Path"

View File

@@ -347,6 +347,77 @@ msgstr "Vas a restaurar %s."
msgid "dashboard-restore-file-confirmation.title"
msgstr "Restaurar archivo"
#: src/app/main/ui/settings/access_tokens.cljs:103
msgid "dashboard.access-tokens.copied-success"
msgstr "Token copiado"
#: src/app/main/ui/settings/access_tokens.cljs:189
msgid "dashboard.access-tokens.create"
msgstr "Generar nuevo token"
#: src/app/main/ui/settings/access_tokens.cljs:64
msgid "dashboard.access-tokens.create.success"
msgstr "Access token creado con éxito."
#: src/app/main/ui/settings/access_tokens.cljs:286
msgid "dashboard.access-tokens.empty.add-one"
msgstr "Pulsa el botón \"Generar nuevo token\" para generar uno."
#: src/app/main/ui/settings/access_tokens.cljs:285
msgid "dashboard.access-tokens.empty.no-access-tokens"
msgstr "Todavía no tienes ningún token."
#: src/app/main/ui/settings/access_tokens.cljs:135
msgid "dashboard.access-tokens.expiration-180-days"
msgstr "180 días"
#: src/app/main/ui/settings/access_tokens.cljs:132
msgid "dashboard.access-tokens.expiration-30-days"
msgstr "30 días"
#: src/app/main/ui/settings/access_tokens.cljs:133
msgid "dashboard.access-tokens.expiration-60-days"
msgstr "60 días"
#: src/app/main/ui/settings/access_tokens.cljs:134
msgid "dashboard.access-tokens.expiration-90-days"
msgstr "90 días"
#: src/app/main/ui/settings/access_tokens.cljs:131
msgid "dashboard.access-tokens.expiration-never"
msgstr "Nunca"
#: src/app/main/ui/settings/access_tokens.cljs:268
msgid "dashboard.access-tokens.expired-on"
msgstr "Expiró el %s"
#: src/app/main/ui/settings/access_tokens.cljs:269
msgid "dashboard.access-tokens.expires-on"
msgstr "Expira el %s"
#: src/app/main/ui/settings/access_tokens.cljs:267
msgid "dashboard.access-tokens.no-expiration"
msgstr "Sin fecha de expiración"
#: src/app/main/ui/settings/access_tokens.cljs:184
msgid "dashboard.access-tokens.personal"
msgstr "Access tokens personales"
#: src/app/main/ui/settings/access_tokens.cljs:185
msgid "dashboard.access-tokens.personal.description"
msgstr ""
"Los access tokens personales funcionan como una alternativa a nuestro "
"sistema de autenticación usuario/password y se pueden usar para permitir a "
"otras aplicaciones acceso a la API interna de Penpot"
#: src/app/main/ui/settings/access_tokens.cljs:142
msgid "dashboard.access-tokens.token-will-expire"
msgstr "El token expirará el %s"
#: src/app/main/ui/settings/access_tokens.cljs:143
msgid "dashboard.access-tokens.token-will-not-expire"
msgstr "El token no tiene fecha de expiración"
#: src/app/main/ui/dashboard/placeholder.cljs:41
msgid "dashboard.add-file"
msgstr "Añadir archivo"
@@ -369,7 +440,7 @@ msgstr "(copia)"
#: src/app/main/ui/dashboard/sidebar.cljs:347
msgid "dashboard.create-new-org"
msgstr "+ Crear org"
msgstr "Crear nueva organización"
#: src/app/main/ui/dashboard/sidebar.cljs:340
msgid "dashboard.create-new-team"
@@ -2034,209 +2105,6 @@ msgstr "Valor resuelto:"
msgid "inspect.tabs.styles.variants-panel"
msgstr "Propiedades de las variantes"
#: src/app/main/ui/settings/integrations.cljs:189
msgid "integrations.access-tokens.create"
msgstr "Crear nuevo token de acceso"
#: src/app/main/ui/settings/integrations.cljs:286
msgid "integrations.access-tokens.empty.add-one"
msgstr "Pulsa el botón \"Crear nuevo token de accesso\" para generar uno."
#: src/app/main/ui/settings/integrations.cljs:285
msgid "integrations.access-tokens.empty.no-access-tokens"
msgstr "Todavía no tienes ningún token."
#: src/app/main/ui/settings/integrations.cljs:184
msgid "integrations.access-tokens.personal"
msgstr "Tokens de acceso personales"
#: src/app/main/ui/settings/integrations.cljs:185
msgid "integrations.access-tokens.personal.description"
msgstr ""
"Los tokens de accesso personales funcionan como una alternativa a nuestro "
"sistema de autenticación usuario/password y se pueden usar para permitir a "
"otras aplicaciones acceso a la API interna de Penpot"
#: src/app/main/ui/settings/integrations.cljs:152, src/app/main/ui/settings/integrations.cljs:158
msgid "integrations.copy-token"
msgstr "Copiar token"
#: src/app/main/ui/settings/integrations.cljs:432
msgid "integrations.create-access-token.title"
msgstr "Crear token de accesso"
#: src/app/main/ui/settings/integrations.cljs:433
msgid "integrations.create-access-token.title.created"
msgstr "Token de acceso creado"
#: src/app/main/ui/settings/integrations.cljs:290
msgid "integrations.create-mcp-key.title"
msgstr "Crear nueva clave MCP"
#: src/app/main/ui/settings/integrations.cljs:291
msgid "integrations.create-mcp-key.title.created"
msgstr "Clave MCP creada"
#: src/app/main/ui/settings/integrations.cljs:257
msgid "integrations.delete-token.accept"
msgstr "Borrar token"
#: src/app/main/ui/settings/integrations.cljs:256
msgid "integrations.delete-token.message"
msgstr "¿Seguro que deseas borrar este token?"
#: src/app/main/ui/settings/integrations.cljs:255
msgid "integrations.delete-token.title"
msgstr "Borrar token"
#: src/app/main/ui/settings/integrations.cljs:135
msgid "integrations.expiration-180-days"
msgstr "180 días"
#: src/app/main/ui/settings/integrations.cljs:132
msgid "integrations.expiration-30-days"
msgstr "30 días"
#: src/app/main/ui/settings/integrations.cljs:133
msgid "integrations.expiration-60-days"
msgstr "60 días"
#: src/app/main/ui/settings/integrations.cljs:134
msgid "integrations.expiration-90-days"
msgstr "90 días"
#: src/app/main/ui/settings/integrations.cljs:131
msgid "integrations.expiration-never"
msgstr "Nunca"
#: src/app/main/ui/settings/integrations.cljs:268
msgid "integrations.expired-on"
msgstr "Expiró el %s"
#: src/app/main/ui/settings/integrations.cljs:269
msgid "integrations.expires-on"
msgstr "Expira el %s"
#: src/app/main/ui/settings/integrations.cljs:267
msgid "integrations.no-expiration"
msgstr "Sin fecha de expiración"
#: src/app/main/ui/settings/integrations.cljs:130
msgid "integrations.expiration-date.label"
msgstr "Fecha de expiración"
#: src/app/main/ui/settings/integrations.cljs:131
msgid "integrations.info.non-recuperable"
msgstr "Esta clave única no es recuperable. Si la pierdes, tendrás que crear una nueva."
#: src/app/main/ui/settings/integrations.cljs:336
msgid "integrations.mcp-server.title"
msgstr "Servidor MCP"
#: src/app/main/ui/settings/integrations.cljs:336
msgid "integrations.mcp-server.title.beta"
msgstr "Beta"
#: src/app/main/ui/settings/integrations.cljs:347
msgid "integrations.mcp-server.description"
msgstr "El servidor MCP de Penpot permite que los clientes MCP interactúen directamente con los archivos de diseño de Penpot."
#: src/app/main/ui/settings/integrations.cljs:353
msgid "integrations.mcp-server.status"
msgstr "Estado"
#: src/app/main/ui/settings/integrations.cljs:370
msgid "integrations.mcp-server.status.enabled"
msgstr "Habilitado"
#: src/app/main/ui/settings/integrations.cljs:370
msgid "integrations.mcp-server.status.disabled"
msgstr "Deshabilitado"
#: src/app/main/ui/settings/integrations.cljs:363
msgid "integrations.mcp-server.status.expired.0"
msgstr "La clave MCP utilizada para conectarse al servidor MCP ha expirado. Como resultado, no se puede establecer la conexión."
#: src/app/main/ui/settings/integrations.cljs:368
msgid "integrations.mcp-server.status.expired.1"
msgstr "Por favor, regenera la clave MCP y actualiza la configuración de tu cliente con la nueva clave."
#: src/app/main/ui/settings/integrations.cljs:415
msgid "integrations.mcp-server.mcp-keys.copy"
msgstr "Copiar enlace"
#: src/app/main/ui/settings/integrations.cljs:422
msgid "integrations.mcp-server.mcp-keys.help"
msgstr "Cómo configurar clientes MCP"
#: src/app/main/ui/settings/integrations.cljs:405
msgid "integrations.mcp-server.mcp-keys.info"
msgstr "Esta es la URL del servidor que necesitarás configurar en tu cliente MCP para conectarlo al servidor MCP de Penpot."
#: src/app/main/ui/settings/integrations.cljs:387
msgid "integrations.mcp-server.mcp-keys.regenerate"
msgstr "Regenerar clave MCP"
#: src/app/main/ui/settings/integrations.cljs:381
msgid "integrations.mcp-server.mcp-keys.title"
msgstr "Clave MCP"
#: src/app/main/ui/settings/integrations.cljs:388
msgid "integrations.mcp-server.mcp-keys.tootip"
msgstr "La clave MCP es necesaria para la configuración del cliente MCP"
#: src/app/main/ui/settings/integrations.cljs:124
msgid "integrations.name.label"
msgstr "Nombre"
#: src/app/main/ui/settings/integrations.cljs:126
msgid "integrations.name.placeholder"
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
#: src/app/main/ui/settings/integrations.cljs:103
msgid "integrations.notification.success.copied"
msgstr "Token copiado"
#: src/app/main/ui/settings/integrations.cljs:64
msgid "integrations.notification.success.created"
msgstr "Token creado con éxito"
#: src/app/main/ui/settings/integrations.cljs:327
msgid "integrations.notification.success.copied-link"
msgstr "Enlace copiado al portapapeles"
#: src/app/main/ui/settings/integrations.cljs:293
msgid "integrations.notification.success.mcp-server-disabled"
msgstr "Servidor MCP deshabilitado"
#: src/app/main/ui/settings/integrations.cljs:299
msgid "integrations.notification.success.mcp-server-enabled"
msgstr "Servidor MCP habilitado"
#: src/app/main/ui/settings/integrations.cljs:317
msgid "integrations.regenerate-mcp-key.info"
msgstr "Regenerar la clave revocará inmediatamente la actual. Cualquier aplicación que la esté utilizando dejará de funcionar."
#: src/app/main/ui/settings/integrations.cljs:317
msgid "integrations.regenerate-mcp-key.title"
msgstr "Regenerar clave MCP"
#: src/app/main/ui/settings/integrations.cljs:318
msgid "integrations.regenerate-mcp-key.title.created"
msgstr "Clave MCP regenerada"
#: src/app/main/ui/settings/integrations.cljs:480
msgid "integrations.title"
msgstr "Integraciones"
#: src/app/main/ui/settings/integrations.cljs:142
msgid "integrations.token-will-expire"
msgstr "El token expirará el %s"
#: src/app/main/ui/settings/integrations.cljs:143
msgid "integrations.token-will-not-expire"
msgstr "El token no tiene fecha de expiración"
#: src/app/main/ui/dashboard/comments.cljs:96
msgid "label.mark-all-as-read"
msgstr "Marcar todo como leído"
@@ -2457,9 +2325,6 @@ msgstr "Descartar"
msgid "labels.download"
msgstr "Descargar %s"
msgid "labels.download-simple"
msgstr "Descargar"
#: src/app/main/ui/dashboard/file_menu.cljs:30, src/app/main/ui/dashboard/files.cljs:80, src/app/main/ui/dashboard/files.cljs:179, src/app/main/ui/dashboard/projects.cljs:229, src/app/main/ui/dashboard/projects.cljs:233, src/app/main/ui/dashboard/sidebar.cljs:820
msgid "labels.drafts"
msgstr "Borradores"
@@ -2591,10 +2456,6 @@ msgstr "Información"
msgid "labels.installed-fonts"
msgstr "Fuentes instaladas"
#: src/app/main/ui/settings/sidebar.cljs:123
msgid "labels.integrations"
msgstr "Integraciones"
#: src/app/main/ui/static.cljs:405
msgid "labels.internal-error.desc-message-first"
msgstr "Ha ocurrido algo extraño."
@@ -3250,6 +3111,30 @@ msgstr "Cambiar correo"
msgid "modals.change-email.title"
msgstr "Cambiar tu correo"
#: src/app/main/ui/settings/access_tokens.cljs:152, src/app/main/ui/settings/access_tokens.cljs:158
msgid "modals.create-access-token.copy-token"
msgstr "Copiar token"
#: src/app/main/ui/settings/access_tokens.cljs:130
msgid "modals.create-access-token.expiration-date.label"
msgstr "Fecha de expiración"
#: src/app/main/ui/settings/access_tokens.cljs:124
msgid "modals.create-access-token.name.label"
msgstr "Nombre"
#: src/app/main/ui/settings/access_tokens.cljs:126
msgid "modals.create-access-token.name.placeholder"
msgstr "El nombre te pude ayudar a saber para qué se utiliza el token"
#: src/app/main/ui/settings/access_tokens.cljs:178
msgid "modals.create-access-token.submit-label"
msgstr "Crear token"
#: src/app/main/ui/settings/access_tokens.cljs:111
msgid "modals.create-access-token.title"
msgstr "Generar access token"
#: src/app/main/ui/dashboard/team.cljs:1127
msgid "modals.create-webhook.submit-label"
msgstr "Crear webhook"
@@ -3266,6 +3151,18 @@ msgstr "Payload URL"
msgid "modals.create-webhook.url.placeholder"
msgstr "https://example.com/postreceive"
#: src/app/main/ui/settings/access_tokens.cljs:257
msgid "modals.delete-acces-token.accept"
msgstr "Borrar token"
#: src/app/main/ui/settings/access_tokens.cljs:256
msgid "modals.delete-acces-token.message"
msgstr "¿Seguro que deseas borrar este token?"
#: src/app/main/ui/settings/access_tokens.cljs:255
msgid "modals.delete-acces-token.title"
msgstr "Borrar token"
#: src/app/main/ui/settings/delete_account.cljs:56
msgid "modals.delete-account.cancel"
msgstr "Cancelar y mantener mi cuenta"
@@ -5171,14 +5068,14 @@ msgstr "Bibliotecas Compartidas - %s - Penpot"
msgid "title.default"
msgstr "Penpot - Diseño Libre para Equipos"
#: src/app/main/ui/settings/access_tokens.cljs:278
msgid "title.settings.access-tokens"
msgstr "Perfil - Access tokens"
#: src/app/main/ui/settings/feedback.cljs:161
msgid "title.settings.feedback"
msgstr "Danos tu opinión - Penpot"
#: src/app/main/ui/settings/integrations.cljs:278
msgid "title.settings.integrations"
msgstr "Integraciones - Penpot"
#: src/app/main/ui/settings/notifications.cljs:45
msgid "title.settings.notifications"
msgstr "Notificaciones - Penpot"
@@ -7644,18 +7541,6 @@ msgstr "Pegar"
msgid "workspace.shape.menu.paste-props"
msgstr "Pegar propiedades"
msgid "workspace.shape.menu.copy-as-image"
msgstr "Copiar como imagen"
msgid "workspace.clipboard.copying"
msgstr "Copiando imagen…"
msgid "workspace.clipboard.image-copied"
msgstr "Imagen copiada al portapapeles"
msgid "workspace.clipboard.image-copy-failed"
msgstr "Error al copiar la imagen"
#: src/app/main/ui/workspace/context_menu.cljs:443
msgid "workspace.shape.menu.path"
msgstr "Ruta"

View File

@@ -1,11 +0,0 @@
interface McpOptions {
getToken(): string;
getServerUrl(): string;
setMcpStatus(status: string);
}
declare global {
const mcp: undefined | McpOptions;
}
export {};

View File

@@ -19,19 +19,12 @@ const statusElement = document.getElementById("connection-status");
* @param isConnectedState - whether the connection is in a connected state (affects color)
* @param message - optional additional message to append to the status
*/
function updateConnectionStatus(code: string, status: string, isConnectedState: boolean, message?: string): void {
function updateConnectionStatus(status: string, isConnectedState: boolean, message?: string): void {
if (statusElement) {
const displayText = message ? `${status}: ${message}` : status;
statusElement.textContent = displayText;
statusElement.style.color = isConnectedState ? "var(--accent-primary)" : "var(--error-700)";
}
parent.postMessage(
{
type: "update-connection-status",
status: code,
},
"*"
);
}
/**
@@ -51,24 +44,25 @@ function sendTaskResponse(response: any): void {
/**
* Establishes a WebSocket connection to the MCP server.
*/
function connectToMcpServer(baseUrl?: string, token?: string): void {
function connectToMcpServer(): void {
if (ws?.readyState === WebSocket.OPEN) {
updateConnectionStatus("connected", "Already connected", true);
updateConnectionStatus("Already connected", true);
return;
}
try {
let wsUrl = baseUrl || PENPOT_MCP_WEBSOCKET_URL;
if (isMultiUserMode && token) {
wsUrl += `?userToken=${encodeURIComponent(token)}`;
let wsUrl = PENPOT_MCP_WEBSOCKET_URL;
if (isMultiUserMode) {
// TODO obtain proper userToken from penpot
const userToken = "dummyToken";
wsUrl += `?userToken=${encodeURIComponent(userToken)}`;
}
ws = new WebSocket(wsUrl);
updateConnectionStatus("connecting", "Connecting...", false);
updateConnectionStatus("Connecting...", false);
ws.onopen = () => {
console.log("Connected to MCP server");
updateConnectionStatus("connected", "Connected to MCP server", true);
updateConnectionStatus("Connected to MCP server", true);
};
ws.onmessage = (event) => {
@@ -85,19 +79,19 @@ function connectToMcpServer(baseUrl?: string, token?: string): void {
ws.onclose = (event: CloseEvent) => {
console.log("Disconnected from MCP server");
const message = event.reason || undefined;
updateConnectionStatus("disconnected", "Disconnected", false, message);
updateConnectionStatus("Disconnected", false, message);
ws = null;
};
ws.onerror = (error) => {
console.error("WebSocket error:", error);
// note: WebSocket error events typically don't contain detailed error messages
updateConnectionStatus("error", "Connection error", false);
updateConnectionStatus("Connection error", false);
};
} catch (error) {
console.error("Failed to connect to MCP server:", error);
const message = error instanceof Error ? error.message : undefined;
updateConnectionStatus("error", "Connection failed", false, message);
updateConnectionStatus("Connection failed", false, message);
}
}
@@ -107,14 +101,10 @@ document.querySelector("[data-handler='connect-mcp']")?.addEventListener("click"
// Listen plugin.ts messages
window.addEventListener("message", (event) => {
if (event.data.type === "start-server") {
connectToMcpServer(event.data.url, event.data.token);
} else if (event.data.source === "penpot") {
if (event.data.source === "penpot") {
document.body.dataset.theme = event.data.theme;
} else if (event.data.type === "task-response") {
// Forward task response back to MCP server
sendTaskResponse(event.data.response);
}
});
parent.postMessage({ type: "ui-initialized" }, "*");

View File

@@ -1,8 +1,6 @@
import { ExecuteCodeTaskHandler } from "./task-handlers/ExecuteCodeTaskHandler";
import { Task, TaskHandler } from "./TaskHandler";
mcp?.setMcpStatus("connecting");
/**
* Registry of all available task handlers.
*/
@@ -13,24 +11,12 @@ declare const IS_MULTI_USER_MODE: boolean;
const isMultiUserMode = typeof IS_MULTI_USER_MODE !== "undefined" ? IS_MULTI_USER_MODE : false;
// Open the plugin UI (main.ts)
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, {
width: 158,
height: 200,
hidden: !!mcp,
} as any);
penpot.ui.open("Penpot MCP Plugin", `?theme=${penpot.theme}&multiUser=${isMultiUserMode}`, { width: 158, height: 200 });
// Handle messages
penpot.ui.onMessage<string | { id: string; type?: string; status?: string; task: string; params: any }>((message) => {
penpot.ui.onMessage<string | { id: string; task: string; params: any }>((message) => {
// Handle plugin task requests
if (mcp && typeof message === "object" && message.type === "ui-initialized") {
penpot.ui.sendMessage({
type: "start-server",
url: mcp?.getServerUrl(),
token: mcp?.getToken(),
});
} else if (typeof message === "object" && message.type === "update-connection-status") {
mcp?.setMcpStatus(message.status || "unknown");
} else if (typeof message === "object" && message.task && message.id) {
if (typeof message === "object" && message.task && message.id) {
handlePluginTaskRequest(message).catch((error) => {
console.error("Error in handlePluginTaskRequest:", error);
});

View File

@@ -4,6 +4,7 @@ import baseConfig from "./vite.config";
export default mergeConfig(
baseConfig,
defineConfig({
base: "./",
plugins: [],
})
);

View File

@@ -23,7 +23,7 @@ export interface Penpot extends Omit<
open: (
name: string,
url: string,
options?: { width: number; height: number; hidden: boolean },
options?: { width: number; height: number },
) => void;
size: {

View File

@@ -6,8 +6,8 @@
"ses": "^1.1.0",
"zod": "^3.22.4"
},
"module": "./dist/index.js",
"typings": "./dist/index.d.ts",
"module": "./index.mjs",
"typings": "./index.d.ts",
"type": "module",
"scripts": {
"build": "vite build",

View File

@@ -24,10 +24,6 @@ export function createModal(
inlineStart: window.innerWidth - width - 290,
};
if (options?.hidden) {
modal.style.setProperty('display', 'none');
}
modal.style.setProperty(
'--modal-block-start',
`${initialPosition.blockStart}px`,

View File

@@ -83,7 +83,7 @@ describe('createPlugin', () => {
expect.any(Function),
expect.any(Function),
);
expect(createSandbox).toHaveBeenCalledWith(mockPluginManager, undefined);
expect(createSandbox).toHaveBeenCalledWith(mockPluginManager);
expect(mockSandbox.evaluate).toHaveBeenCalled();
expect(result).toEqual({
plugin: mockPluginManager,

View File

@@ -7,7 +7,6 @@ export async function createPlugin(
context: Context,
manifest: Manifest,
onCloseCallback: () => void,
apiExtensions?: object,
) {
const evaluateSandbox = async () => {
try {
@@ -31,7 +30,7 @@ export async function createPlugin(
},
);
const sandbox = createSandbox(plugin, apiExtensions);
const sandbox = createSandbox(plugin);
evaluateSandbox();

View File

@@ -2,10 +2,8 @@ import type { Penpot } from '@penpot/plugin-types';
import type { createPluginManager } from './plugin-manager';
import { createApi } from './api';
import { ses } from './ses.js';
export function createSandbox(
plugin: Awaited<ReturnType<typeof createPluginManager>>,
apiExtensions?: object,
) {
ses.hardenIntrinsics();
@@ -53,7 +51,7 @@ export function createSandbox(
});
};
let publicPluginApi = {
const publicPluginApi = {
penpot: proxyApi,
fetch: ses.harden(safeFetch),
setTimeout: ses.harden(
@@ -125,10 +123,6 @@ export function createSandbox(
structuredClone: ses.harden(window.structuredClone),
};
if (apiExtensions) {
publicPluginApi = Object.assign(publicPluginApi, apiExtensions);
}
const compartment = ses.createCompartment(publicPluginApi);
return {

View File

@@ -78,7 +78,6 @@ describe('plugin-loader', () => {
mockContext,
manifest,
expect.any(Function),
undefined,
);
expect(mockPluginApi.plugin.close).not.toHaveBeenCalled();
expect(getPlugins()).toHaveLength(1);
@@ -131,7 +130,6 @@ describe('plugin-loader', () => {
mockContext,
manifest,
expect.any(Function),
undefined,
);
});
@@ -146,7 +144,6 @@ describe('plugin-loader', () => {
mockContext,
manifest,
expect.any(Function),
undefined,
);
});
});

View File

@@ -19,10 +19,7 @@ export const getPlugins = () => plugins;
const closeAllPlugins = () => {
plugins.forEach((pluginApi) => {
/* eslint-disable @typescript-eslint/no-explicit-any */
if (!(pluginApi.manifest as any)?.allowBackground) {
pluginApi.plugin.close();
}
pluginApi.plugin.close();
});
plugins = [];
@@ -41,7 +38,6 @@ window.addEventListener('message', (event) => {
export const loadPlugin = async function (
manifest: Manifest,
closeCallback?: () => void,
apiExtensions?: object,
) {
try {
const context = contextBuilder && contextBuilder(manifest.pluginId);
@@ -62,7 +58,6 @@ export const loadPlugin = async function (
closeCallback();
}
},
apiExtensions,
);
plugins.push(plugin);
@@ -75,9 +70,8 @@ export const loadPlugin = async function (
export const ɵloadPlugin = async function (
manifest: Manifest,
closeCallback?: () => void,
apiExtensions?: object,
) {
loadPlugin(manifest, closeCallback, apiExtensions);
loadPlugin(manifest, closeCallback);
};
export const ɵloadPluginByUrl = async function (manifestUrl: string) {

View File

@@ -3,5 +3,4 @@ import { z } from 'zod';
export const openUISchema = z.object({
width: z.number().positive(),
height: z.number().positive(),
hidden: z.boolean().optional(),
});

873
plugins/pnpm-lock.yaml generated
View File

File diff suppressed because it is too large Load Diff

View File

@@ -831,6 +831,8 @@ impl RenderState {
let text_content = text_content.new_bounds(shape.selrect());
let count_inner_strokes = shape.count_visible_inner_strokes();
let text_stroke_blur_outset =
Stroke::max_bounds_width(shape.visible_strokes(), false);
let mut paragraph_builders = text_content.paragraph_builder_group_from_text(None);
let mut stroke_paragraphs_list = shape
.visible_strokes()
@@ -858,7 +860,7 @@ impl RenderState {
);
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
text::render(
text::render_with_bounds_outset(
Some(self),
None,
&shape,
@@ -866,6 +868,7 @@ impl RenderState {
Some(strokes_surface_id),
None,
None,
text_stroke_blur_outset,
);
}
} else {
@@ -957,7 +960,7 @@ impl RenderState {
// 4. Stroke fills
for stroke_paragraphs in stroke_paragraphs_list.iter_mut() {
text::render(
text::render_with_bounds_outset(
Some(self),
None,
&shape,
@@ -965,6 +968,7 @@ impl RenderState {
Some(strokes_surface_id),
None,
blur_filter.as_ref(),
text_stroke_blur_outset,
);
}

View File

@@ -156,7 +156,8 @@ fn get_text_stroke_paints(
paints
}
pub fn render(
#[allow(clippy::too_many_arguments)]
pub fn render_with_bounds_outset(
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
@@ -164,12 +165,20 @@ pub fn render(
surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
stroke_bounds_outset: f32,
) {
if let Some(render_state) = render_state {
let target_surface = surface_id.unwrap_or(SurfaceId::Fills);
if let Some(blur_filter) = blur {
let bounds = blur_filter.compute_fast_bounds(shape.selrect);
let mut text_bounds = shape
.get_text_content()
.calculate_bounds(shape, false)
.to_rect();
if stroke_bounds_outset > 0.0 {
text_bounds.inset((-stroke_bounds_outset, -stroke_bounds_outset));
}
let bounds = blur_filter.compute_fast_bounds(text_bounds);
if bounds.is_finite() && bounds.width() > 0.0 && bounds.height() > 0.0 {
let blur_filter_clone = blur_filter.clone();
if filters::render_with_filter_surface(
@@ -202,6 +211,27 @@ pub fn render(
}
}
pub fn render(
render_state: Option<&mut RenderState>,
canvas: Option<&Canvas>,
shape: &Shape,
paragraph_builders: &mut [Vec<ParagraphBuilder>],
surface_id: Option<SurfaceId>,
shadow: Option<&Paint>,
blur: Option<&ImageFilter>,
) {
render_with_bounds_outset(
render_state,
canvas,
shape,
paragraph_builders,
surface_id,
shadow,
blur,
0.0,
);
}
fn render_text_on_canvas(
canvas: &Canvas,
shape: &Shape,