Compare commits

..

3 Commits

Author SHA1 Message Date
alonso.torres
3e6e7b0e70 🐛 Fix problem with export dialog on single board 2026-02-20 13:04:06 +01:00
Andrés Moya
3d41dc276e 🐛 Fix resolve tokens with tokenscript when type is font family 2026-02-20 12:41:17 +01:00
alonso.torres
cee974a906 🐛 Fix problem with tokens in plugins 2026-02-18 17:20:46 +01:00
127 changed files with 1767 additions and 14939 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,25 +1,5 @@
# CHANGELOG # CHANGELOG
## 2.15.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :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) ## 2.14.0 (Unreleased)
### :sparkles: New features & Enhancements ### :sparkles: New features & Enhancements
@@ -48,7 +28,6 @@
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174) - 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 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 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 ## 2.13.3

View File

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

View File

@@ -54,7 +54,7 @@
[:path ::fs/path] [:path ::fs/path]
[:mtype {:optional true} ::sm/text]]) [:mtype {:optional true} ::sm/text]])
(def check-input (def ^:private check-input
(sm/check-fn schema:input)) (sm/check-fn schema:input))
(defn validate-media-type! (defn validate-media-type!
@@ -381,22 +381,6 @@
(when (zero? (:exit res)) (when (zero? (:exit res))
(:out 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: ;; Documented here:
;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory ;; https://docs.microsoft.com/en-us/typography/opentype/spec/otff#table-directory
(get-sfnt-type [data] (get-sfnt-type [data]
@@ -446,27 +430,4 @@
(= stype :ttf) (= stype :ttf)
(-> (assoc "font/otf" (ttf->otf sfnt)) (-> (assoc "font/otf" (ttf->otf sfnt))
(assoc "font/ttf" 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))))))))

View File

@@ -463,10 +463,8 @@
:fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")} :fn (mg/resource "app/migrations/sql/0144-mod-server-error-report-table.sql")}
{:name "0145-fix-plugins-uri-on-profile" {: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! (defn apply-migrations!
[pool name migrations] [pool name migrations]

View File

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

View File

@@ -87,10 +87,6 @@
[:map [:map
[:valid ::sm/boolean]]) [:valid ::sm/boolean]])
(def ^:private schema:connectivity
[:map
[:licenses ::sm/boolean]])
(defn- get-team-org (defn- get-team-org
[cfg {:keys [team-id] :as params}] [cfg {:keys [team-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)] (let [baseuri (cf/get :nitrate-backend-uri)]
@@ -101,11 +97,6 @@
(let [baseuri (cf/get :nitrate-backend-uri)] (let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params))) (request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
(defn- get-connectivity
[cfg params]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get (str baseuri "/api/connectivity") schema:connectivity params)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; INITIALIZATION ;; INITIALIZATION
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -114,8 +105,7 @@
[_ cfg] [_ cfg]
(when (contains? cf/flags :nitrate) (when (contains? cf/flags :nitrate)
{:get-team-org (partial get-team-org cfg) {:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg) :is-valid-user (partial is-valid-user cfg)}))
:connectivity (partial get-connectivity cfg)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS ;; UTILS
@@ -138,7 +128,3 @@
(let [params (assoc (or params {}) :team-id (:id team)) (let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)] org (call cfg :get-team-org params)]
(assoc team :organization-id (:id org) :organization-name (:name org)))) (assoc team :organization-id (:id org) :organization-name (:name org))))
(defn connectivity
[cfg]
(call cfg :connectivity {}))

View File

@@ -73,13 +73,9 @@
(if (nil? result) (if (nil? result)
204 204
200)) 200))
headers (cond-> (::http/headers mdata {})
headers (::http/headers mdata {}) (yres/stream-body? result)
headers (cond-> headers
(and (yres/stream-body? result)
(not (contains? headers "content-type")))
(assoc "content-type" "application/octet-stream"))] (assoc "content-type" "application/octet-stream"))]
{::yres/status status {::yres/status status
::yres/headers headers ::yres/headers headers
::yres/body result}))] ::yres/body result}))]
@@ -262,7 +258,6 @@
'app.rpc.commands.ldap 'app.rpc.commands.ldap
'app.rpc.commands.management 'app.rpc.commands.management
'app.rpc.commands.media 'app.rpc.commands.media
'app.rpc.commands.nitrate
'app.rpc.commands.profile 'app.rpc.commands.profile
'app.rpc.commands.projects 'app.rpc.commands.projects
'app.rpc.commands.search 'app.rpc.commands.search

View File

@@ -23,7 +23,7 @@
(dissoc row :perms)) (dissoc row :perms))
(defn create-access-token (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) (let [token-id (uuid/next)
expires-at (some-> expiration (ct/in-future)) expires-at (some-> expiration (ct/in-future))
created-at (ct/now) created-at (ct/now)
@@ -36,7 +36,6 @@
{:id token-id {:id token-id
:name name :name name
:token token :token token
:type type
:profile-id profile-id :profile-id profile-id
:created-at created-at :created-at created-at
:updated-at created-at :updated-at created-at
@@ -51,18 +50,17 @@
(def ^:private schema:create-access-token (def ^:private schema:create-access-token
[:map {:title "create-access-token"} [:map {:title "create-access-token"}
[:name [:string {:max 250 :min 1}]] [:name [:string {:max 250 :min 1}]]
[:expiration {:optional true} ::ct/duration] [:expiration {:optional true} ::ct/duration]])
[:type {:optional true} :string]])
(sv/defmethod ::create-access-token (sv/defmethod ::create-access-token
{::doc/added "1.18" {::doc/added "1.18"
::sm/params schema:create-access-token} ::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/check! cfg {::quotes/id ::quotes/access-tokens-per-profile
::quotes/profile-id profile-id}) ::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 (def ^:private schema:delete-access-token
[:map {:title "delete-access-token"} [:map {:title "delete-access-token"}
@@ -85,22 +83,5 @@
(->> (db/query pool :access-token (->> (db/query pool :access-token
{:profile-id profile-id} {:profile-id profile-id}
{:order-by [[:expires-at :asc] [:created-at :asc]] {: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))) (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.binfile.common :as bfc]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.media :as cmedia]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.common.uuid :as uuid] [app.common.uuid :as uuid]
[app.db :as db] [app.db :as db]
[app.db.sql :as-alias sql] [app.db.sql :as-alias sql]
[app.features.logical-deletion :as ldel] [app.features.logical-deletion :as ldel]
[app.http :as-alias http]
[app.loggers.audit :as-alias audit] [app.loggers.audit :as-alias audit]
[app.loggers.webhooks :as-alias webhooks] [app.loggers.webhooks :as-alias webhooks]
[app.media :as media] [app.media :as media]
@@ -36,9 +34,7 @@
java.io.InputStream java.io.InputStream
java.io.OutputStream java.io.OutputStream
java.io.SequenceInputStream java.io.SequenceInputStream
java.util.Collections java.util.Collections))
java.util.zip.ZipEntry
java.util.zip.ZipOutputStream))
(set! *warn-on-reflection* true) (set! *warn-on-reflection* true)
@@ -300,98 +296,3 @@
(rph/with-meta (rph/wrap) (rph/with-meta (rph/wrap)
{::audit/props {:font-family (:font-family variant) {::audit/props {:font-family (:font-family variant)
:font-id (:font-id 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

@@ -1,20 +0,0 @@
(ns app.rpc.commands.nitrate
(:require
[app.common.schema :as sm]
[app.nitrate :as nitrate]
[app.rpc :as-alias rpc]
[app.rpc.doc :as-alias doc]
[app.util.services :as sv]))
(def schema:connectivity
[:map {:title "nitrate-connectivity"}
[:licenses ::sm/boolean]])
(sv/defmethod ::get-nitrate-connectivity
{::rpc/auth false
::doc/added "1.18"
::sm/params [:map]
::sm/result schema:connectivity}
[cfg _params]
(nitrate/connectivity cfg))

View File

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

View File

@@ -102,7 +102,7 @@
(t/deftest access-token-authz (t/deftest access-token-authz
(let [profile (th/create-profile* 1) (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*)] handler (#'app.http.access-token/wrap-authz identity th/*system*)]
(let [response (handler nil)] (let [response (handler nil)]

View File

@@ -107,18 +107,4 @@
;; (th/print-result! out) ;; (th/print-result! out)
(t/is (nil? (:error out))) (t/is (nil? (:error out)))
(let [results (:result out)] (let [results (:result out)]
(t/is (= 2 (count results)))))) (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)))))))

View File

@@ -93,41 +93,6 @@
:font-weight :font-weight
:font-style)))) :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 (t/deftest font-deletion-1
(let [prof (th/create-profile* 1 {:is-active true}) (let [prof (th/create-profile* 1 {:is-active true})
team-id (:default-team-id prof) team-id (:default-team-id prof)

View File

Binary file not shown.

View File

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

View File

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

View File

@@ -9,39 +9,6 @@
[app.common.media :as media] [app.common.media :as media]
[clojure.test :as t])) [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/deftest test-strip-image-extension
(t/testing "removes extension from supported image files" (t/testing "removes extension from supported image files"
(t/is (= (media/strip-image-extension "foo.png") "foo")) (t/is (= (media/strip-image-extension "foo.png") "foo"))

View File

@@ -100,12 +100,14 @@
(def browser-pool-factory (def browser-pool-factory
(letfn [(create [] (letfn [(create []
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]} (-> (p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
browser (.launch pw/chromium opts) browser (.launch pw/chromium opts)
id (swap! pool-browser-id inc)] id (swap! pool-browser-id inc)]
(l/info :origin "factory" :action "create" :browser-id id) (l/info :origin "factory" :action "create" :browser-id id)
(unchecked-set browser "__id" id) (unchecked-set browser "__id" id)
browser)) browser)
(p/catch (fn [cause]
(l/error :hint "Cannot launch the headless browser" :cause cause)))))
(destroy [obj] (destroy [obj]
(let [id (unchecked-get obj "__id")] (let [id (unchecked-get obj "__id")]

View File

@@ -47,12 +47,13 @@
(s/def ::params (s/def ::params
(s/keys :req-un [::exports ::profile-id] (s/keys :req-un [::exports ::profile-id]
:opt-un [::wait ::name ::skip-children])) :opt-un [::wait ::name ::skip-children ::as-many]))
(defn handler (defn handler
[{:keys [:request/auth-token] :as exchange} {:keys [exports] :as params}] [{:keys [:request/auth-token] :as exchange} {:keys [exports force-multiple] :as params}]
(let [exports (prepare-exports exports auth-token)] (let [exports (prepare-exports exports auth-token)]
(if (and (= 1 (count exports)) (if (and (not force-multiple)
(= 1 (count exports))
(= 1 (count (-> exports first :objects)))) (= 1 (count (-> exports first :objects))))
(handle-single-export exchange (-> params (handle-single-export exchange (-> params
(assoc :export (first exports)) (assoc :export (first exports))

View File

@@ -40,16 +40,14 @@
"watch:app:libs": "node ./scripts/build-libs.js --watch", "watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook", "watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs", "clear:shadow-cache": "rm -rf .shadow-cljs",
"clear:wasm": "cargo clean --manifest-path ../render-wasm/Cargo.toml",
"watch": "exit 0", "watch": "exit 0",
"watch:app": "pnpm run clear:shadow-cache && pnpm run clear:wasm && 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:app": "pnpm run clear:shadow-cache && pnpm run build:wasm && concurrently --kill-others-on-fail \"pnpm run watch:app:assets\" \"pnpm run watch:app:main\" \"pnpm run watch:app:libs\"",
"watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\"", "watch:storybook": "pnpm run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
"postinstall": "(cd ../plugins/libs/plugins-runtime; pnpm install; pnpm run build)"
}, },
"devDependencies": { "devDependencies": {
"@penpot/draft-js": "workspace:./packages/draft-js", "@penpot/draft-js": "workspace:./packages/draft-js",
"@penpot/mousetrap": "workspace:./packages/mousetrap", "@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/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "workspace:./text-editor", "@penpot/text-editor": "workspace:./text-editor",
"@penpot/tokenscript": "workspace:./packages/tokenscript", "@penpot/tokenscript": "workspace:./packages/tokenscript",

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +0,0 @@
{
"~:file-id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~:id": "~u4173a29d-4020-80b4-8007-96527ba9d8af",
"~:created-at": "~m1771342236330",
"~:modified-at": "~m1771342236330",
"~:type": "fragment",
"~:backend": "db",
"~:data": {
"~:id": "~uecb0cfd0-0f0b-81f7-8007-950628f9665b",
"~:name": "Page 1",
"~:objects": {
"~#penpot/objects-map/v2": {
"~ude9c6736-45ce-80a1-8007-950643da554d": "[\"~#shape\",[\"^ \",\"~:y\",383.99998939037323,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"R1\",\"~:width\",74,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",383.99998939037323]],[\"^<\",[\"^ \",\"~:x\",905.999992787838,\"~:y\",383.99998939037323]],[\"^<\",[\"^ \",\"~:x\",905.999992787838,\"~:y\",429.99998664855957]],[\"^<\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",429.99998664855957]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-950643da554d\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:strokes\",[],\"~:x\",831.999992787838,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",831.999992787838,\"~:y\",383.99998939037323,\"^8\",74,\"~:height\",45.99999725818634,\"~:x1\",831.999992787838,\"~:y1\",383.99998939037323,\"~:x2\",905.999992787838,\"~:y2\",429.99998664855957]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^K\",45.99999725818634,\"~:flip-y\",null]]",
"~ude9c6736-45ce-80a1-8007-95065b4599ea": "[\"~#shape\",[\"^ \",\"~:y\",439.999969256975,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"R2\",\"~:width\",300.00007388362076,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",832.0000015918991,\"~:y\",439.99996925697496]],[\"^<\",[\"^ \",\"~:x\",1132.00007547552,\"~:y\",439.99996925697496]],[\"^<\",[\"^ \",\"~:x\",1132.00007547552,\"~:y\",589.9999658711585]],[\"^<\",[\"^ \",\"~:x\",832.0000015918991,\"~:y\",589.9999658711585]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"^?\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95065b4599ea\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:strokes\",[],\"~:x\",832.0000015918993,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",832.0000015918993,\"~:y\",439.999969256975,\"^8\",300.00007388362076,\"~:height\",149.9999966141835,\"~:x1\",832.0000015918993,\"~:y1\",439.999969256975,\"~:x2\",1132.0000754755201,\"~:y2\",589.9999658711586]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^N\",149.9999966141835,\"~:flip-y\",null]]",
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^I\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\"]]]",
"~ude9c6736-45ce-80a1-8007-95062aa41d6b": "[\"~#shape\",[\"^ \",\"~:y\",342.0000208153068,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",false,\"~:name\",\"A\",\"~:layout-align-items\",\"~:start\",\"~:width\",392.9999889135361,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",342.0000208153068]],[\"^L\",[\"^ \",\"~:x\",1180.9999653100967,\"~:y\",342.0000208153068]],[\"^L\",[\"^ \",\"~:x\",1180.9999653100967,\"~:y\",704.000018450968]],[\"^L\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",704.000018450968]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",787.9999763965607,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",787.9999763965607,\"~:y\",342.0000208153068,\"^F\",392.9999889135361,\"~:height\",361.9999976356612,\"~:x1\",787.9999763965607,\"~:y1\",342.0000208153068,\"~:x2\",1180.9999653100967,\"~:y2\",704.000018450968]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^19\",361.9999976356612,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95062fafbb88\"]]]",
"~ude9c6736-45ce-80a1-8007-95063a202e52": "[\"~#shape\",[\"^ \",\"~:y\",374.00001145409465,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",10,\"~:p2\",10,\"~:p3\",10,\"~:p4\",10],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"C\",\"~:layout-align-items\",\"~:start\",\"~:width\",320.00009969013945,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",374.0000114540946]],[\"^L\",[\"^ \",\"~:x\",1142.0000912532628,\"~:y\",374.0000114540946]],[\"^L\",[\"^ \",\"~:x\",1142.0000912532628,\"~:y\",600.0000518384436]],[\"^L\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",600.0000518384436]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",10,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"^O\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95063a202e52\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:strokes\",[[\"^ \",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-style\",\"~:solid\",\"~:stroke-color\",\"#000000\",\"~:stroke-opacity\",1,\"~:stroke-width\",1]],\"~:x\",821.9999915631233,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",821.9999915631233,\"~:y\",374.00001145409465,\"^F\",320.00009969013945,\"~:height\",226.000040384349,\"~:x1\",821.9999915631233,\"~:y1\",374.00001145409465,\"~:x2\",1142.0000912532628,\"~:y2\",600.0000518384436]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1A\",226.000040384349,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95065b4599ea\",\"~ude9c6736-45ce-80a1-8007-950643da554d\"]]]",
"~ude9c6736-45ce-80a1-8007-95062fafbb88": "[\"~#shape\",[\"^ \",\"~:y\",342.0000083402533,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",31.999953508377075,\"~:p2\",34.000026052944804,\"~:p3\",31.999953508377075,\"~:p4\",34.000026052944804],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"B\",\"~:layout-align-items\",\"~:start\",\"~:width\",392.9999979687366,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",342.00000834025326]],[\"^L\",[\"^ \",\"~:x\",1181.0000275138177,\"~:y\",342.00000834025326]],[\"^L\",[\"^ \",\"~:x\",1181.0000275138177,\"~:y\",631.9999659712]],[\"^L\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",631.9999659712]]],\"~:r2\",0,\"~:layout-item-h-sizing\",\"~:fill\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~uecb0cfd0-0f0b-81f7-8007-950628f9665b\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~ude9c6736-45ce-80a1-8007-95062fafbb88\",\"~:parent-id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ude9c6736-45ce-80a1-8007-95062aa41d6b\",\"~:strokes\",[[\"^ \",\"~:stroke-alignment\",\"~:inner\",\"~:stroke-style\",\"~:solid\",\"~:stroke-color\",\"#000000\",\"~:stroke-opacity\",1,\"~:stroke-width\",1]],\"~:x\",788.0000295450811,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",788.0000295450811,\"~:y\",342.0000083402533,\"^F\",392.9999979687366,\"~:height\",289.99995763094677,\"~:x1\",788.0000295450811,\"~:y1\",342.0000083402533,\"~:x2\",1181.0000275138177,\"~:y2\",631.9999659712]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1B\",289.99995763094677,\"~:flip-y\",null,\"~:shapes\",[\"~ude9c6736-45ce-80a1-8007-95063a202e52\"]]]"
}
}
}
}

View File

@@ -1,131 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~ud715d0a5-a44e-8056-8005-a79999e18b64",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "bug flex",
"~:revn": 33,
"~:modified-at": "~m1771342236324",
"~:vern": 0,
"~:id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~u76eab896-accf-81a5-8007-2b264ebe7817",
"~:created-at": "~m1771255281717",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~uecb0cfd0-0f0b-81f7-8007-950628f9665b"
],
"~:pages-index": {
"~uecb0cfd0-0f0b-81f7-8007-950628f9665b": {
"~#penpot/pointer": [
"~u4173a29d-4020-80b4-8007-96527ba9d8af",
{
"~:created-at": "~m1771342236327"
}
]
}
},
"~:id": "~u52c4e771-3853-8190-8007-9506c70e8100",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,136 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "BUG 13385",
"~:revn": 3,
"~:modified-at": "~m1771254407745",
"~:vern": 1173241426,
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771254391625",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u3ea49ce0-9d99-8197-8007-950361d24e44"
],
"~:pages-index": {
"~u3ea49ce0-9d99-8197-8007-950361d24e44": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~ue0e81bed-4dc2-805c-8007-95036c27428b\"]]]",
"~ue0e81bed-4dc2-805c-8007-95036a4a3131": "[\"~#shape\",[\"^ \",\"~:y\",252,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",177,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",156,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",156,\"~:y\",389]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",156,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",156,\"~:y\",252,\"^8\",177,\"~:height\",137,\"~:x1\",156,\"~:y1\",252,\"~:x2\",333,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]",
"~ue0e81bed-4dc2-805c-8007-95036c27428b": "[\"~#shape\",[\"^ \",\"~:y\",250,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Ellipse\",\"~:width\",148,\"~:type\",\"~:circle\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",362,\"~:y\",250]],[\"^<\",[\"^ \",\"~:x\",510,\"~:y\",250]],[\"^<\",[\"^ \",\"~:x\",510,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",362,\"~:y\",389]]],\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036c27428b\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",362,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",362,\"~:y\",250,\"^8\",148,\"~:height\",139,\"~:x1\",362,\"~:y1\",250,\"~:x2\",510,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^F\",139,\"~:flip-y\",null]]"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e44",
"~:name": "Page 1"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,135 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "BUG 13385",
"~:revn": 2,
"~:modified-at": "~m1771254464312",
"~:vern": 0,
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771254391625",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u3ea49ce0-9d99-8197-8007-950361d24e44"
],
"~:pages-index": {
"~u3ea49ce0-9d99-8197-8007-950361d24e44": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\"]]]",
"~ue0e81bed-4dc2-805c-8007-95036a4a3131": "[\"~#shape\",[\"^ \",\"~:y\",252,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",177,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",156,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",252]],[\"^<\",[\"^ \",\"~:x\",333,\"~:y\",389]],[\"^<\",[\"^ \",\"~:x\",156,\"~:y\",389]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~ue0e81bed-4dc2-805c-8007-95036a4a3131\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",156,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",156,\"~:y\",252,\"^8\",177,\"~:height\",137,\"~:x1\",156,\"~:y1\",252,\"~:x2\",333,\"~:y2\",389]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e44",
"~:name": "Page 1"
}
},
"~:id": "~u3ea49ce0-9d99-8197-8007-950361d24e43",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,135 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~u99e49e93-362f-80ef-8007-3450ea52c9a4",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "BUG 13415",
"~:revn": 14,
"~:modified-at": "~m1771334256704",
"~:vern": 0,
"~:id": "~u0472abff-2573-8186-8007-961793e53f45",
"~:is-shared": false,
"~:migrations": {
"~#ordered-set": [
"legacy-2",
"legacy-3",
"legacy-5",
"legacy-6",
"legacy-7",
"legacy-8",
"legacy-9",
"legacy-10",
"legacy-11",
"legacy-12",
"legacy-13",
"legacy-14",
"legacy-16",
"legacy-17",
"legacy-18",
"legacy-19",
"legacy-25",
"legacy-26",
"legacy-27",
"legacy-28",
"legacy-29",
"legacy-31",
"legacy-32",
"legacy-33",
"legacy-34",
"legacy-36",
"legacy-37",
"legacy-38",
"legacy-39",
"legacy-40",
"legacy-41",
"legacy-42",
"legacy-43",
"legacy-44",
"legacy-45",
"legacy-46",
"legacy-47",
"legacy-48",
"legacy-49",
"legacy-50",
"legacy-51",
"legacy-52",
"legacy-53",
"legacy-54",
"legacy-55",
"legacy-56",
"legacy-57",
"legacy-59",
"legacy-62",
"legacy-65",
"legacy-66",
"legacy-67",
"0001-remove-tokens-from-groups",
"0002-normalize-bool-content-v2",
"0002-clean-shape-interactions",
"0003-fix-root-shape",
"0003-convert-path-content-v2",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags",
"0010-fix-swap-slots-pointing-non-existent-shapes",
"0011-fix-invalid-text-touched-flags",
"0012-fix-position-data",
"0013-fix-component-path",
"0013-clear-invalid-strokes-and-fills",
"0014-fix-tokens-lib-duplicate-ids",
"0014-clear-components-nil-objects",
"0015-fix-text-attrs-blank-strings",
"0015-clean-shadow-color",
"0016-copy-fills-from-position-data-to-text-node"
]
},
"~:version": 67,
"~:project-id": "~ucd8f7672-e5d1-810f-8007-87e124eda82a",
"~:created-at": "~m1771326794644",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u0472abff-2573-8186-8007-961793e53f46"
],
"~:pages-index": {
"~u0472abff-2573-8186-8007-961793e53f46": {
"~:objects": {
"~#penpot/objects-map/v2": {
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~uaef184da-e9c1-80f8-8007-961cf253d534\"]]]",
"~uaef184da-e9c1-80f8-8007-961cf253d534": "[\"~#shape\",[\"^ \",\"~:y\",286,\"~:layout-grid-columns\",[[\"^ \",\"~:type\",\"~:flex\",\"~:value\",1],[\"^ \",\"^2\",\"^3\",\"^4\",1]],\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:grid\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:layout-align-items\",\"~:start\",\"~:width\",298,\"~:layout-grid-cells\",[\"^ \",\"~uaef184da-e9c1-80f8-8007-961cf50d67b4\",[\"^ \",\"~:justify-self\",\"~:auto\",\"~:column\",1,\"~:id\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b4\",\"~:position\",\"^L\",\"~:column-span\",1,\"~:align-self\",\"^L\",\"~:row\",1,\"~:row-span\",1,\"~:shapes\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b5\",[\"^ \",\"^K\",\"^L\",\"^M\",2,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b5\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",1,\"^S\",1,\"^T\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b6\",[\"^ \",\"^K\",\"^L\",\"^M\",1,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b6\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",2,\"^S\",1,\"^T\",[]],\"~uaef184da-e9c1-80f8-8007-961cf50d67b7\",[\"^ \",\"^K\",\"^L\",\"^M\",2,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf50d67b7\",\"^O\",\"^L\",\"^P\",1,\"^Q\",\"^L\",\"^R\",2,\"^S\",1,\"^T\",[]]],\"~:layout-padding-type\",\"~:simple\",\"^2\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",322,\"~:y\",286]],[\"^10\",[\"^ \",\"~:x\",620,\"~:y\",286]],[\"^10\",[\"^ \",\"~:x\",620,\"~:y\",552]],[\"^10\",[\"^ \",\"~:x\",322,\"~:y\",552]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^>\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:layout-justify-content\",\"~:stretch\",\"~:r1\",0,\"^N\",\"~uaef184da-e9c1-80f8-8007-961cf253d534\",\"~:layout-justify-items\",\"^G\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-align-content\",\"^19\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",322,\"~:proportion\",1,\"~:r4\",0,\"~:layout-grid-rows\",[[\"^ \",\"^2\",\"^3\",\"^4\",1],[\"^ \",\"^2\",\"^3\",\"^4\",1]],\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",322,\"~:y\",286,\"^H\",298,\"~:height\",266,\"~:x1\",322,\"~:y1\",286,\"~:x2\",620,\"~:y2\",552]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:layout-grid-dir\",\"^R\",\"~:flip-x\",null,\"^1E\",266,\"~:flip-y\",null,\"^T\",[]]]"
}
},
"~:id": "~u0472abff-2573-8186-8007-961793e53f46",
"~:name": "Page 1"
}
},
"~:id": "~u0472abff-2573-8186-8007-961793e53f45",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1,23 +0,0 @@
[
{
"~:id": "~u3ea49ce0-9d99-8197-8007-95037190405b",
"~:label": "Version 000",
"~:revn": 1,
"~:version": 67,
"~:created-at": "~m1771254407745",
"~:modified-at": "~m1771254407745",
"~:created-by": "user",
"~:profile-id": "~u99e49e93-362f-80ef-8007-3450ea5204aa"
},
{
"~:revn": 0,
"~:modified-at": "~m1771254406526",
"~:deleted-at": "~m1771340806524",
"~:created-by": "system",
"~:label": "internal/snapshot/0",
"~:id": "~u3ea49ce0-9d99-8197-8007-9503705f8b9b",
"~:profile-id": "~u99e49e93-362f-80ef-8007-3450ea5204aa",
"~:version": 67,
"~:created-at": "~m1771254406526"
}
]

View File

@@ -1,9 +0,0 @@
[
{
"~:id": "~u99e49e93-362f-80ef-8007-3450ea5204aa",
"~:email": "belen@example.com",
"~:name": "Belén Albeza",
"~:fullname": "Belén Albeza",
"~:is-active": true
}
]

View File

@@ -1,18 +0,0 @@
{
"~:revn": 14,
"~:lagged": [
{
"~:id": "~u0472abff-2573-8186-8007-96347d525f65",
"~:revn": 15,
"~:file-id": "~u0472abff-2573-8186-8007-961793e53f45",
"~:session-id": "~uf25e6d2f-d10c-8021-8007-96344433f08d",
"~:changes": [
{
"~:type": "~:del-obj",
"~:page-id": "~u0472abff-2573-8186-8007-961793e53f46",
"~:id": "~uaef184da-e9c1-80f8-8007-961cf253d534"
}
]
}
]
}

View File

@@ -22,7 +22,7 @@ export class BasePage {
* @param {*} options * @param {*} options
* @returns {Promise<void>} * @returns {Promise<void>}
*/ */
static async mockRPC(page, path, jsonFilename = "", options = {}) { static async mockRPC(page, path, jsonFilename, options) {
if (!page) { if (!page) {
throw new TypeError("Invalid page argument. Must be a Playwright page."); throw new TypeError("Invalid page argument. Must be a Playwright page.");
} }
@@ -41,7 +41,7 @@ export class BasePage {
return page.route(url, (route) => return page.route(url, (route) =>
route.fulfill({ route.fulfill({
...interceptConfig, ...interceptConfig,
path: jsonFilename ? `playwright/data/${jsonFilename}` : undefined, path: `playwright/data/${jsonFilename}`,
}), }),
); );
} }

View File

@@ -35,8 +35,8 @@ export class WasmWorkspacePage extends WorkspacePage {
return WasmWorkspacePage.mockConfigFlags(this.page, flags); return WasmWorkspacePage.mockConfigFlags(this.page, flags);
} }
constructor(page, options) { constructor(page) {
super(page, options); super(page);
this.canvas = page.getByTestId("canvas-wasm-shapes"); this.canvas = page.getByTestId("canvas-wasm-shapes");
} }
@@ -54,19 +54,6 @@ export class WasmWorkspacePage extends WorkspacePage {
await this.hideUI(); await this.hideUI();
} }
async getRenderCount() {
return this.page.evaluate(() => window.wasmRenderCount || 0);
}
async waitForNextRender(previousCount = null) {
const baseCount =
previousCount === null ? await this.getRenderCount() : previousCount;
await this.page.waitForFunction(
(count) => (window.wasmRenderCount || 0) > count,
baseCount,
);
}
async hideUI() { async hideUI() {
await this.page.keyboard.press("\\"); await this.page.keyboard.press("\\");
await expect(this.pageName).not.toBeVisible(); await expect(this.pageName).not.toBeVisible();

View File

@@ -35,9 +35,45 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
async waitForEditor() { async waitForEditor() {
const typographyInput = return this.page.waitForSelector('[data-itype="editor"]');
this.workspacePage.rightSidebar.getByLabel("Font Size"); }
await expect(typographyInput).toBeVisible();
async waitForRoot() {
return this.page.waitForSelector('[data-itype="root"]');
}
async waitForParagraph(nth) {
if (!nth) {
return this.page.waitForSelector('[data-itype="paragraph"]');
}
return this.page.waitForSelector(
`[data-itype="paragraph"]:nth-child(${nth})`,
);
}
async waitForParagraphStyle(nth, styleName) {
const paragraph = await this.waitForParagraph(nth);
return this.waitForStyle(paragraph, styleName);
}
async waitForTextSpan(nth = 0) {
if (!nth) {
return this.page.waitForSelector('[data-itype="span"]');
}
return this.page.waitForSelector(
`[data-itype="span"]:nth-child(${nth})`,
);
}
async waitForTextSpanContent(nth = 0) {
const textSpan = await this.waitForTextSpan(nth);
const textContent = await textSpan.textContent();
return textContent;
}
async waitForTextSpanStyle(nth, styleName) {
const textSpan = await this.waitForTextSpan(nth);
return this.waitForStyle(textSpan, styleName);
} }
async startEditing() { async startEditing() {
@@ -62,7 +98,7 @@ export class WorkspacePage extends BaseWebSocketPage {
} }
async moveFromStart(offset = 0) { async moveFromStart(offset = 0) {
await this.page.keyboard.press("Home"); await this.page.keyboard.press("ArrowLeft");
await this.moveToRight(offset); await this.moveToRight(offset);
} }
@@ -89,7 +125,7 @@ export class WorkspacePage extends BaseWebSocketPage {
await expect(locator).toBeVisible(); await expect(locator).toBeVisible();
await locator.focus(); await locator.focus();
await locator.fill(`${newValue}`); await locator.fill(`${newValue}`);
await this.page.keyboard.press("Enter"); await locator.blur();
} }
changeFontSize(newValue) { changeFontSize(newValue) {
@@ -281,6 +317,7 @@ export class WorkspacePage extends BaseWebSocketPage {
body, body,
}), }),
); );
// await this.mockRPC(/get\-file\?/, jsonFile);
} }
async mockGetAsset(regex, asset) { async mockGetAsset(regex, asset) {
@@ -354,12 +391,10 @@ export class WorkspacePage extends BaseWebSocketPage {
const timeToWait = options?.timeToWait ?? 100; const timeToWait = options?.timeToWait ?? 100;
await this.page.keyboard.press("T"); await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait); await this.page.waitForTimeout(timeToWait);
const layersCountBefore = await this.layers.getByTestId("layer-row").count();
await this.clickAndMove(x1, y1, x2, y2); await this.clickAndMove(x1, y1, x2, y2);
await expect(this.page.getByTestId("text-editor")).toBeVisible();
if (initialText) { if (initialText) {
await this.waitForSelectedShapeName("Text");
await this.page.keyboard.type(initialText); await this.page.keyboard.type(initialText);
} }
} }
@@ -459,23 +494,10 @@ export class WorkspacePage extends BaseWebSocketPage {
async expectSelectedLayer(name) { async expectSelectedLayer(name) {
await expect( await expect(
this.layers.getByRole("checkbox", { name, checked: true }), this.layers
).toBeVisible(); .getByTestId("layer-row")
} .filter({ has: this.page.getByText(name) }),
).toHaveClass(/selected/);
async getSelectedShapeName() {
const selectedLayer = this.layers
.getByRole("checkbox", { checked: true })
.first();
await selectedLayer.waitFor({ state: "visible" });
return (await selectedLayer.innerText()).trim();
}
async waitForSelectedShapeName(expectedName) {
const selectedLayer = this.layers
.getByRole("checkbox", { checked: true })
.first();
await expect(selectedLayer).toHaveText(expectedName);
} }
async expectHiddenToolbarOptions() { async expectHiddenToolbarOptions() {

View File

@@ -356,39 +356,3 @@ test("Renders shapes with multiple fills and blur", async ({
await expect(workspace.canvas).toHaveScreenshot(); await expect(workspace.canvas).toHaveScreenshot();
}); });
test("Keeps component visible when focusing after creating it", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockRPC(/get\-file\?/, "workspace/get-file-not-empty.json");
await workspace.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspace.goToWorkspace({
fileId: "6191cd35-bb1f-81f7-8004-7cc63d087374",
pageId: "6191cd35-bb1f-81f7-8004-7cc63d087375",
});
await workspace.waitForFirstRender();
await workspace.clickLayers();
await workspace.clickLeafLayer("Rectangle");
await page.keyboard.press("ControlOrMeta+k");
const componentLayer = workspace.layers
.getByTestId("layer-row")
.filter({ has: page.getByTestId("icon-component") })
.first();
await expect(componentLayer).toBeVisible();
await componentLayer.click();
const previousRenderCount = await workspace.getRenderCount();
await page.keyboard.press("f");
await workspace.waitForNextRender(previousRenderCount);
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 KiB

After

Width:  |  Height:  |  Size: 348 KiB

View File

@@ -1,14 +1,14 @@
import { test, expect } from "@playwright/test"; import { test, expect } from "@playwright/test";
import { Clipboard } from "../../helpers/Clipboard"; import { Clipboard } from "../../helpers/Clipboard";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage"; import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100; const timeToWait = 100;
test.beforeEach(async ({ page, context }) => { test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL); await Clipboard.enable(context, Clipboard.Permission.ALL);
await WasmWorkspacePage.init(page); await WorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]); await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
}); });
test.afterEach(async ({ context }) => { test.afterEach(async ({ context }) => {
@@ -17,21 +17,22 @@ test.afterEach(async ({ context }) => {
test("Create a new text shape", async ({ page }) => { test("Create a new text shape", async ({ page }) => {
const initialText = "Lorem ipsum"; const initialText = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.goToWorkspace(); await workspace.goToWorkspace();
await workspace.createTextShape(190, 150, 300, 200, initialText); await workspace.createTextShape(190, 150, 300, 200, initialText);
await workspace.textEditor.stopEditing(); const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(initialText);
await workspace.waitForSelectedShapeName(initialText); await workspace.textEditor.stopEditing();
}); });
test("Create a new text shape from pasting text", async ({ page, context }) => { test("Create a new text shape from pasting text", async ({ page, context }) => {
const textToPaste = "Lorem ipsum"; const textToPaste = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -42,9 +43,13 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
await workspace.clickAt(190, 150); await workspace.clickAt(190, 150);
await workspace.paste("keyboard"); await workspace.paste("keyboard");
await workspace.textEditor.stopEditing();
await expect(workspace.layers.getByText(textToPaste)).toBeVisible(); await page.waitForTimeout(timeToWait);
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
}); });
test("Create a new text shape from pasting text using context menu", async ({ test("Create a new text shape from pasting text using context menu", async ({
@@ -52,7 +57,7 @@ test("Create a new text shape from pasting text using context menu", async ({
context, context,
}) => { }) => {
const textToPaste = "Lorem ipsum"; const textToPaste = "Lorem ipsum";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -62,15 +67,17 @@ test("Create a new text shape from pasting text using context menu", async ({
await workspace.clickAt(190, 150); await workspace.clickAt(190, 150);
await workspace.paste("context-menu"); await workspace.paste("context-menu");
await workspace.textEditor.stopEditing();
await expect(workspace.layers.getByText(textToPaste)).toBeVisible(); const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
}); });
test("Update an already created text shape by appending text", async ({ test("Update an already created text shape by appending text", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -80,14 +87,15 @@ test("Update an already created text shape by appending text", async ({
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd(0); await workspace.textEditor.moveFromEnd(0);
await page.keyboard.type(" dolor sit amet"); await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet");
}); });
test("Update an already created text shape by prepending text", async ({ test("Update an already created text shape by prepending text", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -97,14 +105,15 @@ test("Update an already created text shape by prepending text", async ({
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(0); await workspace.textEditor.moveFromStart(0);
await page.keyboard.type("Dolor sit amet "); await page.keyboard.type("Dolor sit amet ");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Dolor sit amet Lorem ipsum");
}); });
test.skip("Update an already created text shape by inserting text in between", async ({ test.skip("Update an already created text shape by inserting text in between", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -114,8 +123,9 @@ test.skip("Update an already created text shape by inserting text in between", a
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(5); await workspace.textEditor.moveFromStart(5);
await page.keyboard.type(" dolor sit amet"); await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet ipsum");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem dolor sit amet ipsum");
}); });
test("Update a new text shape appending text by pasting text", async ({ test("Update a new text shape appending text by pasting text", async ({
@@ -123,7 +133,7 @@ test("Update a new text shape appending text by pasting text", async ({
context, context,
}) => { }) => {
const textToPaste = " dolor sit amet"; const textToPaste = " dolor sit amet";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -136,16 +146,17 @@ test("Update a new text shape appending text by pasting text", async ({
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd(); await workspace.textEditor.moveFromEnd();
await workspace.paste("keyboard"); await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet"); });
});
test.skip("Update a new text shape prepending text by pasting text", async ({ test.skip("Update a new text shape prepending text by pasting text", async ({
page, page,
context, context,
}) => { }) => {
const textToPaste = "Dolor sit amet "; const textToPaste = "Dolor sit amet ";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -158,17 +169,16 @@ test.skip("Update a new text shape prepending text by pasting text", async ({
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(); await workspace.textEditor.moveFromStart();
await workspace.paste("keyboard"); await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
}); });
test("Update a new text shape replacing (starting) text with pasted text", async ({ test("Update a new text shape replacing (starting) text with pasted text", async ({
page, page,
}) => { }) => {
const textToPaste = "Dolor sit amet"; const textToPaste = "Dolor sit amet";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -182,15 +192,17 @@ test("Update a new text shape replacing (starting) text with pasted text", async
await workspace.paste("keyboard"); await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet ipsum");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Dolor sit amet ipsum");
}); });
test("Update a new text shape replacing (ending) text with pasted text", async ({ test("Update a new text shape replacing (ending) text with pasted text", async ({
page, page,
}) => { }) => {
const textToPaste = "dolor sit amet"; const textToPaste = "dolor sit amet";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -204,15 +216,17 @@ test("Update a new text shape replacing (ending) text with pasted text", async (
await workspace.paste("keyboard"); await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem dolor sit amet");
}); });
test("Update a new text shape replacing (in between) text with pasted text", async ({ test("Update a new text shape replacing (in between) text with pasted text", async ({
page, page,
}) => { }) => {
const textToPaste = "dolor sit amet"; const textToPaste = "dolor sit amet";
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -226,14 +240,16 @@ test("Update a new text shape replacing (in between) text with pasted text", asy
await workspace.paste("keyboard"); await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lordolor sit ametsum");
await workspace.textEditor.stopEditing(); await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lordolor sit ametsum");
}); });
test("Update text font size selecting a part of it (starting)", async ({ test("Update text font size selecting a part of it (starting)", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -244,16 +260,18 @@ test("Update text font size selecting a part of it (starting)", async ({
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5); await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeFontSize(36); await workspace.textEditor.changeFontSize(36);
await workspace.textEditor.stopEditing();
await workspace.hideUI(); const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
await expect(workspace.canvas).toHaveScreenshot(); expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
}); });
test("Update text line height selecting a part of it (starting)", async ({ test.skip("Update text line height selecting a part of it (starting)", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -263,17 +281,24 @@ test("Update text line height selecting a part of it (starting)", async ({
await workspace.clickLeafLayer("Lorem ipsum"); await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5); await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLineHeight(4.4); await workspace.textEditor.changeLineHeight(1.4);
await workspace.textEditor.stopEditing();
await workspace.hideUI(); const lineHeight = await workspace.textEditor.waitForParagraphStyle(
await expect(workspace.canvas).toHaveScreenshot(); 1,
"line-height",
);
expect(lineHeight).toBe("1.4");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum");
await workspace.textEditor.stopEditing();
}); });
test.skip("Update text letter spacing selecting a part of it (starting)", async ({ test.skip("Update text letter spacing selecting a part of it (starting)", async ({
page, page,
}) => { }) => {
const workspace = new WasmWorkspacePage(page, { const workspace = new WorkspacePage(page, {
textEditor: true, textEditor: true,
}); });
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
@@ -284,14 +309,16 @@ test.skip("Update text letter spacing selecting a part of it (starting)", async
await workspace.textEditor.startEditing(); await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5); await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLetterSpacing(10); await workspace.textEditor.changeLetterSpacing(10);
await workspace.textEditor.stopEditing();
await workspace.hideUI(); const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
await expect(workspace.canvas).toHaveScreenshot(); expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
}); });
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => { test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
const workspace = new WasmWorkspacePage(page); const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile(); await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-11552.json"); await workspace.mockGetFile("text-editor/get-file-11552.json");
await workspace.mockRPC( await workspace.mockRPC(

View File

@@ -112,31 +112,3 @@ test("BUG 11006 - Fix history panel shortcut", async ({ page }) => {
workspacePage.rightSidebar.getByText("There are no versions yet"), workspacePage.rightSidebar.getByText("There are no versions yet"),
).toBeVisible(); ).toBeVisible();
}); });
test("BUG 13385 - Fix viewport not updating when restoring version", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13385.json");
await workspacePage.mockRPC("get-profiles-for-file-comments?file-id=*", "workspace/get-profiles-for-file-comments-13385.json");
// navigate to workspace and check that the circle shape is not there
await workspacePage.goToWorkspace();
await expect(workspacePage.layers.getByText("Ellipse")).not.toBeVisible();
// mock network requests to restore the version
await workspacePage.mockGetFile("workspace/get-file-13385-2.json");
await workspacePage.mockRPC("get-file-snapshots?file-id=*", "workspace/get-file-snapshots-13385.json");
await workspacePage.mockRPC("restore-file-snapshot", "", {
status: 204,
});
// request to restore the version
await workspacePage.rightSidebar.getByRole("button", { name: "History" }).click();
await workspacePage.rightSidebar.getByRole("button", { name: "Open version menu" }).click();
await workspacePage.rightSidebar.getByRole("button", { name: "Restore" }).click();
// confirm modal
await workspacePage.page.getByRole("button", { name: /Restore/i }).click();
// assert that the circle shape exists
await expect(workspacePage.layers.getByText("Ellipse")).toBeVisible();
});

View File

@@ -23,35 +23,4 @@ test("BUG 13305 - Fix resize board to fit content", async ({ page }) => {
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("630"); await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("630");
await expect(workspacePage.rightSidebar.getByTitle("X axis").getByRole("textbox")).toHaveValue("110"); await expect(workspacePage.rightSidebar.getByTitle("X axis").getByRole("textbox")).toHaveValue("110");
await expect(workspacePage.rightSidebar.getByTitle("Y axis").getByRole("textbox")).toHaveValue("110"); await expect(workspacePage.rightSidebar.getByTitle("Y axis").getByRole("textbox")).toHaveValue("110");
}); });
test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13382.json");
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-13382-fragment.json",
);
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
await workspacePage.goToWorkspace({
fileId: "52c4e771-3853-8190-8007-9506c70e8100",
pageId: "ecb0cfd0-0f0b-81f7-8007-950628f9665b",
});
await workspacePage.clickToggableLayer("A");
await workspacePage.clickToggableLayer("B");
await workspacePage.clickToggableLayer("C");
await workspacePage.clickLeafLayer("R2");
const heightText = workspacePage.rightSidebar.getByTitle("Height").getByPlaceholder('--');
await heightText.fill("200");
await heightText.press("Enter");
await workspacePage.clickLeafLayer("B");
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
});

View File

@@ -483,25 +483,3 @@ test("Bug 8371 - Flatten option is not visible in context menu", async ({
.filter({ visible: true }), .filter({ visible: true }),
).toBeVisible(); ).toBeVisible();
}); });
test("BUG 13415 - Grid layout overlay is not removed when deleting a board", async ({
page,
}) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile(page);
await workspacePage.mockGetFile("workspace/get-file-13415.json");
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-13415.json",
);
await workspacePage.goToWorkspace();
await workspacePage.clickLeafLayer("Board");
const currentRenderCount = await workspacePage.getRenderCount();
await workspacePage.page.keyboard.press("Delete");
await workspacePage.waitForNextRender(currentRenderCount);
await workspacePage.hideUI();
await expect(workspacePage.canvas).toHaveScreenshot();
});

View File

@@ -20,8 +20,8 @@ importers:
specifier: workspace:./packages/mousetrap specifier: workspace:./packages/mousetrap
version: link:packages/mousetrap version: link:packages/mousetrap
'@penpot/plugins-runtime': '@penpot/plugins-runtime':
specifier: link:../plugins/dist/plugins-runtime specifier: 1.4.2
version: link:../plugins/dist/plugins-runtime version: 1.4.2
'@penpot/svgo': '@penpot/svgo':
specifier: penpot/svgo#v3.2 specifier: penpot/svgo#v3.2
version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b version: svgo@https://codeload.github.com/penpot/svgo/tar.gz/8c9b0e32e9cb5f106085260bd9375f3c91a5010b
@@ -581,6 +581,15 @@ packages:
'@dabh/diagnostics@2.0.8': '@dabh/diagnostics@2.0.8':
resolution: {integrity: sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==} 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': '@esbuild/aix-ppc64@0.21.5':
resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
@@ -1249,6 +1258,12 @@ packages:
resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==}
engines: {node: '>= 10.0.0'} 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': '@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -4621,6 +4636,9 @@ packages:
resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'} engines: {node: '>= 18'}
ses@1.14.0:
resolution: {integrity: sha512-T07hNgOfVRTLZGwSS50RnhqrG3foWP+rM+Q5Du4KUQyMLFI3A8YA4RKl0jjZzhihC1ZvDGrWi/JMn4vqbgr/Jg==}
set-function-length@1.2.2: set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -5481,6 +5499,9 @@ packages:
peerDependencies: peerDependencies:
zod: ^3.25.0 || ^4.0.0 zod: ^3.25.0 || ^4.0.0
zod@3.25.76:
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
zod@4.3.6: zod@4.3.6:
resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==}
@@ -5754,6 +5775,12 @@ snapshots:
enabled: 2.0.0 enabled: 2.0.0
kuler: 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': '@esbuild/aix-ppc64@0.21.5':
optional: true optional: true
@@ -6270,6 +6297,14 @@ snapshots:
'@parcel/watcher-win32-x64': 2.5.6 '@parcel/watcher-win32-x64': 2.5.6
optional: true 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': '@pkgjs/parseargs@0.11.0':
optional: true optional: true
@@ -9965,6 +10000,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - 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: set-function-length@1.2.2:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -10933,4 +10974,6 @@ snapshots:
dependencies: dependencies:
zod: 4.3.6 zod: 4.3.6
zod@3.25.76: {}
zod@4.3.6: {} zod@4.3.6: {}

View File

@@ -119,10 +119,6 @@
(normalize-uri (or (obj/get global "penpotPublicURI") (normalize-uri (or (obj/get global "penpotPublicURI")
(obj/get location "origin")))) (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 (def rasterizer-uri
(or (some-> (obj/get global "penpotRasterizerURI") normalize-uri) (or (some-> (obj/get global "penpotRasterizerURI") normalize-uri)
public-uri)) public-uri))
@@ -151,9 +147,6 @@
(let [f (obj/get global "initializeExternalConfigInfo")] (let [f (obj/get global "initializeExternalConfigInfo")]
(when (fn? f) (f)))) (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 ;; --- Helper Functions
(defn ^boolean check-browser? [candidate] (defn ^boolean check-browser? [candidate]

View File

@@ -195,7 +195,7 @@
params {:exports exports params {:exports exports
:cmd cmd :cmd cmd
:profile-id profile-id :profile-id profile-id
:wait false} :force-multiple true}
progress-stream progress-stream
(->> (ws/get-rcv-stream ws-conn) (->> (ws/get-rcv-stream ws-conn)

View File

@@ -99,65 +99,46 @@
map with temporal ID's associated to each font entry." map with temporal ID's associated to each font entry."
[blobs team-id] [blobs team-id]
(letfn [(prepare [{:keys [font type name data] :as params}] (letfn [(prepare [{:keys [font type name data] :as params}]
(if font (let [family (or (.getEnglishName ^js font "preferredFamily")
;; Font was parsed with opentype.js (ttf, otf, woff) (.getEnglishName ^js font "fontFamily"))
(let [family (or (.getEnglishName ^js font "preferredFamily") variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontFamily")) (.getEnglishName ^js font "fontSubfamily"))
variant (or (.getEnglishName ^js font "preferredSubfamily")
(.getEnglishName ^js font "fontSubfamily"))
;; Vertical metrics determine the baseline in a text and the space between lines of ;; 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 ;; 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 ;; 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 ;; 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 ;; 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 ;; 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 ;; 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 ;; table. On Windows, all browsers use the usWin metrics, but respect the
;; useTypoMetrics setting and if set will use the OS/2 values. ;; useTypoMetrics setting and if set will use the OS/2 values.
hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender)) hhea-ascender (abs (-> ^js font .-tables .-hhea .-ascender))
hhea-descender (abs (-> ^js font .-tables .-hhea .-descender)) hhea-descender (abs (-> ^js font .-tables .-hhea .-descender))
win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent)) win-ascent (abs (-> ^js font .-tables .-os2 .-usWinAscent))
win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent)) win-descent (abs (-> ^js font .-tables .-os2 .-usWinDescent))
os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender)) os2-ascent (abs (-> ^js font .-tables .-os2 .-sTypoAscender))
os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender)) os2-descent (abs (-> ^js font .-tables .-os2 .-sTypoDescender))
;; useTypoMetrics can be read from the 7th bit ;; useTypoMetrics can be read from the 7th bit
f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7)) f-selection (-> ^js font .-tables .-os2 .-fsSelection (bit-test 7))
height-warning? (or (not= hhea-ascender win-ascent) height-warning? (or (not= hhea-ascender win-ascent)
(not= hhea-descender win-descent) (not= hhea-descender win-descent)
(and f-selection (or (and f-selection (or
(not= hhea-ascender os2-ascent) (not= hhea-ascender os2-ascent)
(not= hhea-descender os2-descent)))) (not= hhea-descender os2-descent))))
data (js/Uint8Array. data)] data (js/Uint8Array. data)]
{:content {:data (chunk-array data default-chunk-size) {:content {:data (chunk-array data default-chunk-size)
:name name :name name
:type type} :type type}
:font-family (or family "") :font-family (or family "")
:font-weight (cm/parse-font-weight variant) :font-weight (cm/parse-font-weight variant)
:font-style (cm/parse-font-style variant) :font-style (cm/parse-font-style variant)
:height-warning? height-warning?}) :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})))
(join [res {:keys [content] :as font}] (join [res {:keys [content] :as font}]
(let [key-fn (juxt :font-family :font-weight :font-style) (let [key-fn (juxt :font-family :font-weight :font-style)
@@ -185,18 +166,14 @@
(case sg (case sg
"117 124 124 117" "font/otf" "117 124 124 117" "font/otf"
"0 1 0 0" "font/ttf" "0 1 0 0" "font/ttf"
"167 117 106 106" "font/woff" "167 117 106 106" "font/woff")))
"167 117 106 62" "font/woff2")))
(parse-font [{:keys [data type name] :as params}] (parse-font [{:keys [data] :as params}]
(if (= type "font/woff2") (try
;; woff2 cannot be parsed by opentype.js, extract metadata from filename (assoc params :font (ot/parse data))
(assoc params :font nil) (catch :default _e
(try (log/warn :msg (str/fmt "skipping file %s, unsupported format" (:name params)))
(assoc params :font (ot/parse data)) nil)))
(catch :default _e
(log/warn :msg (str/fmt "skipping file %s, unsupported format" name))
nil))))
(read-blob [blob] (read-blob [blob]
(->> (wa/read-file-as-array-buffer blob) (->> (wa/read-file-as-array-buffer blob)

View File

@@ -1,15 +0,0 @@
(ns app.main.data.nitrate
(:require
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn show-nitrate-popup
[]
(ptk/reify ::show-nitrate-popup
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! ::get-nitrate-connectivity {})
(rx/map (fn [connectivity]
(modal/show :nitrate-form (or connectivity {}))))))))

View File

@@ -65,23 +65,8 @@
(update [_ state] (update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id)))) (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! (defn- load-plugin!
[{:keys [plugin-id name description host code icon permissions] :as params}] [{:keys [plugin-id name description host code icon permissions]}]
(try (try
(st/emit! (save-current-plugin plugin-id) (st/emit! (save-current-plugin plugin-id)
(reset-plugin-flags plugin-id)) (reset-plugin-flags plugin-id))

View File

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

View File

@@ -79,7 +79,7 @@
Structured tokens are non-primitive token types like `typography` or `box-shadow`." Structured tokens are non-primitive token types like `typography` or `box-shadow`."
[^js token-symbol] [^js token-symbol]
(if (instance? js/Array (.-value token-symbol)) (if (instance? js/Array (.-value token-symbol))
(mapv structured-token->penpot-map (.-value token-symbol)) (mapv tokenscript-symbols->penpot-unit (.-value token-symbol))
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))] (let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
(into {} (map (fn [[k v :as V]] (into {} (map (fn [[k v :as V]]
[(keyword k) (tokenscript-symbols->penpot-unit v)]) [(keyword k) (tokenscript-symbols->penpot-unit v)])
@@ -88,7 +88,7 @@
(defn tokenscript-symbols->penpot-unit [^js v] (defn tokenscript-symbols->penpot-unit [^js v]
(cond (cond
(structured-token? v) (structured-token->penpot-map v) (structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v)) (list-symbol? v) (structured-token->penpot-map v)
(color-symbol? v) (.-value (.to v "hex")) (color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v) (rem-number-with-unit? v) (rem->px v)
:else (.-value v))) :else (.-value v)))

View File

@@ -52,7 +52,6 @@
[app.main.data.workspace.layers :as dwly] [app.main.data.workspace.layers :as dwly]
[app.main.data.workspace.layout :as layout] [app.main.data.workspace.layout :as layout]
[app.main.data.workspace.libraries :as dwl] [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.notifications :as dwn]
[app.main.data.workspace.pages :as dwpg] [app.main.data.workspace.pages :as dwpg]
[app.main.data.workspace.path :as dwdp] [app.main.data.workspace.path :as dwdp]
@@ -213,8 +212,7 @@
ptk/WatchEvent ptk/WatchEvent
(watch [_ _ _] (watch [_ _ _]
(rx/of (dp/check-open-plugin) (rx/of (dp/check-open-plugin)
(fdf/fix-deleted-fonts-for-local-library file-id) (fdf/fix-deleted-fonts-for-local-library file-id)))))
(mcp/init-mcp-connexion)))))
(defn- bundle-fetched (defn- bundle-fetched
[{:keys [file file-id thumbnails] :as bundle}] [{:keys [file file-id thumbnails] :as bundle}]
@@ -224,16 +222,9 @@
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(let [pending-version-id (:workspace-pending-file-version-id state) (-> state
state (-> state (assoc :thumbnails thumbnails)
(assoc :thumbnails thumbnails) (update :files assoc file-id file)))))
(update :files assoc file-id file)
(dissoc :workspace-pending-file-version-id))]
(cond-> state
(some? pending-version-id)
(assoc :workspace-file-version-id pending-version-id)
(nil? pending-version-id)
(dissoc :workspace-file-version-id))))))
(defn zoom-to-frame (defn zoom-to-frame
[] []
@@ -289,197 +280,192 @@
(wasm.api/process-object shape)))))) (wasm.api/process-object shape))))))
(defn initialize-workspace (defn initialize-workspace
([team-id file-id] [team-id file-id]
(initialize-workspace team-id file-id nil)) (assert (uuid? team-id) "expected valud uuid for `team-id`")
([team-id file-id version-id] (assert (uuid? file-id) "expected valud uuid for `file-id`")
(assert (uuid? team-id) "expected valud uuid for `team-id`")
(assert (uuid? file-id) "expected valud uuid for `file-id`")
(ptk/reify ::initialize-workspace (ptk/reify ::initialize-workspace
ptk/UpdateEvent ptk/UpdateEvent
(update [_ state] (update [_ state]
(-> state (-> state
(assoc :recent-colors (:recent-colors storage/user)) (assoc :recent-colors (:recent-colors storage/user))
(assoc :recent-fonts (:recent-fonts storage/user)) (assoc :recent-fonts (:recent-fonts storage/user))
(assoc :current-file-id file-id) (assoc :current-file-id file-id)
(assoc :workspace-presence {}) (assoc :workspace-presence {})))
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
;; when the new bundle is applied so the viewport re-inits with new data
(assoc :workspace-pending-file-version-id version-id)))
ptk/WatchEvent ptk/WatchEvent
(watch [_ state stream] (watch [_ state stream]
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream) (let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
rparams (rt/get-params state) rparams (rt/get-params state)
features (features/get-enabled-features state team-id) features (features/get-enabled-features state team-id)
render-wasm? (contains? features "render-wasm/v1")] render-wasm? (contains? features "render-wasm/v1")]
(log/debug :hint "initialize-workspace" (log/debug :hint "initialize-workspace"
:team-id (dm/str team-id) :team-id (dm/str team-id)
:file-id (dm/str file-id)) :file-id (dm/str file-id))
(->> (rx/merge (->> (rx/merge
(rx/concat (rx/concat
;; Fetch all essential data that should be loaded before the file ;; Fetch all essential data that should be loaded before the file
(rx/merge (rx/merge
(if ^boolean render-wasm? (if ^boolean render-wasm?
(->> (rx/from @wasm/module) (->> (rx/from @wasm/module)
(rx/filter true?) (rx/filter true?)
(rx/tap (fn [_] (rx/tap (fn [_]
(let [event (ug/event "penpot:wasm:loaded")] (let [event (ug/event "penpot:wasm:loaded")]
(ug/dispatch! event)))) (ug/dispatch! event))))
(rx/ignore)) (rx/ignore))
(rx/empty)) (rx/empty))
(->> stream (->> stream
(rx/filter (ptk/type? ::df/fonts-loaded)) (rx/filter (ptk/type? ::df/fonts-loaded))
(rx/take 1) (rx/take 1)
(rx/ignore)) (rx/ignore))
(rx/of (ntf/hide) (rx/of (ntf/hide)
(dcmt/retrieve-comment-threads file-id) (dcmt/retrieve-comment-threads file-id)
(dcmt/fetch-profiles) (dcmt/fetch-profiles)
(df/fetch-fonts team-id))) (df/fetch-fonts team-id)))
;; Once the essential data is fetched, lets proceed to ;; Once the essential data is fetched, lets proceed to
;; fetch teh file bunldle ;; fetch teh file bunldle
(rx/of (fetch-bundle file-id features))) (rx/of (fetch-bundle file-id features)))
(->> stream (->> stream
(rx/filter (ptk/type? ::bundle-fetched)) (rx/filter (ptk/type? ::bundle-fetched))
(rx/take 1) (rx/take 1)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [file]}] (fn [{:keys [file]}]
(log/debug :hint "bundle fetched" (log/debug :hint "bundle fetched"
:team-id (dm/str team-id) :team-id (dm/str team-id)
:file-id (dm/str file-id)) :file-id (dm/str file-id))
(rx/of (dpj/initialize-project (:project-id file)) (rx/of (dpj/initialize-project (:project-id file))
(dwn/initialize team-id file-id) (dwn/initialize team-id file-id)
(dwsl/initialize-shape-layout) (dwsl/initialize-shape-layout)
(fetch-libraries file-id features) (fetch-libraries file-id features)
(-> (workspace-initialized file-id) (-> (workspace-initialized file-id)
(with-meta {:team-id team-id (with-meta {:team-id team-id
:file-id file-id})))))) :file-id file-id}))))))
;; Install dev perf observers once the workspace is ready ;; Install dev perf observers once the workspace is ready
(when (contains? cf/flags :perf-logs) (when (contains? cf/flags :perf-logs)
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/take 1) (rx/take 1)
(rx/tap (fn [_] (perf/setup))))) (rx/tap (fn [_] (perf/setup)))))
(->> stream (->> stream
(rx/filter (ptk/type? ::dps/persistence-notification)) (rx/filter (ptk/type? ::dps/persistence-notification))
(rx/take 1) (rx/take 1)
(rx/map dwc/set-workspace-visited)) (rx/map dwc/set-workspace-visited))
(when-let [component-id (some-> rparams :component-id uuid/parse)] (when-let [component-id (some-> rparams :component-id uuid/parse)]
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async) (rx/observe-on :async)
(rx/take 1) (rx/take 1)
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams))))) (rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
(when (:board-id rparams) (when (:board-id rparams)
(->> stream (->> stream
(rx/filter (ptk/type? ::dwv/initialize-viewport)) (rx/filter (ptk/type? ::dwv/initialize-viewport))
(rx/take 1) (rx/take 1)
(rx/map zoom-to-frame))) (rx/map zoom-to-frame)))
(when-let [comment-id (some-> rparams :comment-id uuid/parse)] (when-let [comment-id (some-> rparams :comment-id uuid/parse)]
(->> stream (->> stream
(rx/filter (ptk/type? ::workspace-initialized)) (rx/filter (ptk/type? ::workspace-initialized))
(rx/observe-on :async) (rx/observe-on :async)
(rx/take 1) (rx/take 1)
(rx/map #(dwcm/navigate-to-comment-id comment-id)))) (rx/map #(dwcm/navigate-to-comment-id comment-id))))
(when render-wasm? (when render-wasm?
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [redo-changes]}] (fn [{:keys [redo-changes]}]
(let [added (->> redo-changes (let [added (->> redo-changes
(filter #(= (:type %) :add-obj)) (filter #(= (:type %) :add-obj))
(map :id))] (map :id))]
(->> (rx/from added) (->> (rx/from added)
(rx/map process-wasm-object))))))) (rx/map process-wasm-object)))))))
(when render-wasm? (when render-wasm?
(let [local-commits-s (let [local-commits-s
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/filter #(and (= :local (:source %)) (rx/filter #(and (= :local (:source %))
(not (contains? (:tags %) :position-data)))) (not (contains? (:tags %) :position-data))))
(rx/filter (complement empty?))) (rx/filter (complement empty?)))
notifier-s notifier-s
(rx/merge (rx/merge
(->> local-commits-s (rx/debounce 1000)) (->> local-commits-s (rx/debounce 1000))
(->> stream (rx/filter dps/force-persist?))) (->> stream (rx/filter dps/force-persist?)))
objects-s objects-s
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true}) (rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
current-page-id-s current-page-id-s
(rx/from-atom refs/current-page-id {:emit-current-value? true})] (rx/from-atom refs/current-page-id {:emit-current-value? true})]
(->> local-commits-s (->> local-commits-s
(rx/buffer-until notifier-s) (rx/buffer-until notifier-s)
(rx/with-latest-from objects-s) (rx/with-latest-from objects-s)
(rx/map (rx/map
(fn [[commits objects]] (fn [[commits objects]]
(->> commits (->> commits
(mapcat :redo-changes) (mapcat :redo-changes)
(filter #(contains? #{:mod-obj :add-obj} (:type %))) (filter #(contains? #{:mod-obj :add-obj} (:type %)))
(filter #(cfh/text-shape? objects (:id %))) (filter #(cfh/text-shape? objects (:id %)))
(map #(vector (map #(vector
(:id %) (:id %)
(wasm.api/calculate-position-data (get objects (:id %)))))))) (wasm.api/calculate-position-data (get objects (:id %))))))))
(rx/with-latest-from current-page-id-s) (rx/with-latest-from current-page-id-s)
(rx/map (rx/map
(fn [[text-position-data page-id]] (fn [[text-position-data page-id]]
(let [changes (let [changes
(->> text-position-data (->> text-position-data
(mapv (fn [[id position-data]] (mapv (fn [[id position-data]]
{:type :mod-obj {:type :mod-obj
:id id :id id
:page-id page-id :page-id page-id
:operations :operations
[{:type :set [{:type :set
:attr :position-data :attr :position-data
:val position-data :val position-data
:ignore-touched true :ignore-touched true
:ignore-geometry true}]})))] :ignore-geometry true}]})))]
(when (d/not-empty? changes) (when (d/not-empty? changes)
(dch/commit-changes (dch/commit-changes
{:redo-changes changes :undo-changes [] {:redo-changes changes :undo-changes []
:save-undo? false :save-undo? false
:tags #{:position-data}}))))) :tags #{:position-data}})))))
(rx/take-until stoper-s)))) (rx/take-until stoper-s))))
(->> stream (->> stream
(rx/filter dch/commit?) (rx/filter dch/commit?)
(rx/map deref) (rx/map deref)
(rx/mapcat (rx/mapcat
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}] (fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo?]}]
(if (and save-undo? (seq undo-changes)) (if (and save-undo? (seq undo-changes))
(let [entry {:undo-changes undo-changes (let [entry {:undo-changes undo-changes
:redo-changes redo-changes :redo-changes redo-changes
:undo-group undo-group :undo-group undo-group
:tags tags}] :tags tags}]
(rx/of (dwu/append-undo entry stack-undo?))) (rx/of (dwu/append-undo entry stack-undo?)))
(rx/empty)))))) (rx/empty))))))
(rx/take-until stoper-s)))) (rx/take-until stoper-s))))
ptk/EffectEvent ptk/EffectEvent
(effect [_ _ _] (effect [_ _ _]
(let [name (dm/str "workspace-" file-id)] (let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))) (unchecked-set ug/global "name" name)))))
(defn finalize-workspace (defn finalize-workspace
[_team-id file-id] [_team-id file-id]
@@ -1448,7 +1434,6 @@
(dm/export dwcp/paste-shapes) (dm/export dwcp/paste-shapes)
(dm/export dwcp/paste-data-valid?) (dm/export dwcp/paste-data-valid?)
(dm/export dwcp/copy-link-to-clipboard) (dm/export dwcp/copy-link-to-clipboard)
(dm/export dwcp/copy-as-image)
;; Drawing ;; Drawing
(dm/export dwd/select-for-drawing) (dm/export dwd/select-for-drawing)

View File

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

View File

@@ -1,60 +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]
[app.plugins.register :refer [mcp-plugin-id]]
[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 mcp-plugin-id
:description "This plugin enables interaction with the Penpot MCP server"
:allow-background true
:permissions
#{"library:read" "library:write"
"comment:read" "comment:write"
"content: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

@@ -108,7 +108,7 @@
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/tap #(th/clear-queue!)) (rx/tap #(th/clear-queue!))
(rx/map #(dw/initialize-workspace team-id file-id id))) (rx/map #(dw/initialize-workspace team-id file-id)))
(case origin (case origin
:version :version
(rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"})) (rx/of (ptk/event ::ev/event {::ev/name "restore-pin-version"}))
@@ -231,7 +231,7 @@
(rx/filter #(or (nil? %) (= :saved %))) (rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1) (rx/take 1)
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id})) (rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-workspace team-id file-id id))) (rx/map #(dw/initialize-workspace team-id file-id)))
(->> (rx/of 1) (->> (rx/of 1)
(rx/tap resolve) (rx/tap resolve)

View File

@@ -256,9 +256,6 @@
(def workspace-layout (def workspace-layout
(l/derived :workspace-layout st/state)) (l/derived :workspace-layout st/state))
(def workspace-file-version-id
(l/derived :workspace-file-version-id st/state))
(def snap-pixel? (def snap-pixel?
(l/derived #(contains? % :snap-pixel-grid) workspace-layout)) (l/derived #(contains? % :snap-pixel-grid) workspace-layout))

View File

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

View File

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

View File

@@ -32,7 +32,6 @@
input-name (get props :name) input-name (get props :name)
more-classes (get props :class) more-classes (get props :class)
auto-focus? (get props :auto-focus? false) auto-focus? (get props :auto-focus? false)
input-ref (mf/use-ref nil)
data-testid (d/nilv data-testid input-name) data-testid (d/nilv data-testid input-name)
@@ -83,6 +82,7 @@
(swap! form assoc-in [:touched input-name] true) (swap! form assoc-in [:touched input-name] true)
(fm/on-input-change form input-name value trim) (fm/on-input-change form input-name value trim)
(on-change-value name value))) (on-change-value name value)))
on-blur on-blur
(fn [_] (fn [_]
(reset! focus? false)) (reset! focus? false))
@@ -92,18 +92,9 @@
(when-not (get-in @form [:touched input-name]) (when-not (get-in @form [:touched input-name])
(swap! form assoc-in [:touched input-name] true))) (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 props (-> props
(dissoc :help-icon :form :trim :children :show-success? :auto-focus? :label) (dissoc :help-icon :form :trim :children :show-success? :auto-focus? :label)
(assoc :id (name input-name) (assoc :id (name input-name)
:ref input-ref
:value value :value value
:auto-focus auto-focus? :auto-focus auto-focus?
:on-click (when (or is-radio? is-checkbox?) on-click) :on-click (when (or is-radio? is-checkbox?) on-click)
@@ -140,7 +131,7 @@
:for (name input-name)} label :for (name input-name)} label
(when is-checkbox? (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? (if is-checkbox?
[:> :input props] [:> :input props]

View File

@@ -9,17 +9,14 @@
(:require (:require
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.store :as st] [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.ds.notifications.context-notification :refer [context-notification*]]
[app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as k] [app.util.keyboard :as k]
[goog.events :as events] [goog.events :as events]
[rumext.v2 :as mf]) [rumext.v2 :as mf])
(:import (:import goog.events.EventType))
goog.events.EventType))
(mf/defc confirm-dialog (mf/defc confirm-dialog
{::mf/register modal/components {::mf/register modal/components
@@ -71,11 +68,8 @@
[:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)} [:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} title] [:h2 {:class (stl/css :modal-title)} title]
[:div {:class (stl/css :modal-close-btn)} [:button {:class (stl/css :modal-close-btn)
[:> icon-button* {:variant "ghost" :on-click cancel-fn} deprecated-icon/close]]
:aria-label (tr "labels.close")
:on-click cancel-fn
:icon i/close}]]]
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
(when (and (string? message) (not= message "")) (when (and (string? message) (not= message ""))
@@ -93,19 +87,24 @@
[:ul {:class (stl/css :component-list)} [:ul {:class (stl/css :component-list)}
(for [item items] (for [item items]
[:li {:class (stl/css :modal-item-element)} [:li {:class (stl/css :modal-item-element)}
[:> icon* {:icon-id i/component [:span {:class (stl/css :modal-component-icon)}
:class (stl/css :modal-component-icon) deprecated-icon/component]
:size "s"}]
[:span {:class (stl/css :modal-component-name)} [:span {:class (stl/css :modal-component-name)}
(:name item)]])]])] (:name item)]])]])]
[:div {:class (stl/css :modal-footer)} [:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)} [:div {:class (stl/css :action-buttons)}
(when-not (= cancel-label :omit) (when-not (= cancel-label :omit)
[:> button* {:variant "secondary" [:input
:on-click cancel-fn} {:class (stl/css :cancel-button)
cancel-label]) :type "button"
[:> button* {:variant (cond (= accept-style :danger) "destructive" :value cancel-label
(= accept-style :primary) "primary") :on-click cancel-fn}])
:on-click accept-fn}
accept-label]]]]])) [: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 { .modal-container {
@extend .modal-container-base; @extend .modal-container-base;
display: flex; }
flex-direction: column;
gap: var(--sp-xxl); .modal-header {
margin-bottom: deprecated.$s-24;
} }
.modal-title { .modal-title {
@@ -26,13 +27,12 @@
} }
.modal-close-btn { .modal-close-btn {
position: absolute; @extend .modal-close-btn-base;
top: var(--sp-m);
right: var(--sp-m);
} }
.modal-content { .modal-content {
@include deprecated.bodyLargeTypography; @include deprecated.bodyLargeTypography;
margin-bottom: deprecated.$s-24;
} }
.modal-item-element { .modal-item-element {
@@ -41,18 +41,32 @@
.modal-component-icon { .modal-component-icon {
@include deprecated.flexCenter; @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 { .modal-component-name {
@include deprecated.bodyLargeTypography; @include deprecated.bodyLargeTypography;
color: var(--color-foreground-secondary);
} }
.action-buttons { .action-buttons {
@extend .modal-action-btns; @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-scd-msg,
.modal-subtitle, .modal-subtitle,
.modal-msg { .modal-msg {

View File

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

View File

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

View File

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

View File

@@ -40,11 +40,6 @@
overflow-y: auto; overflow-y: auto;
} }
.sidebar-content-nitrate {
padding: var(--sp-m) 0 0 0;
border-block-start: $b-1 solid var(--color-background-quaternary);
}
.separator { .separator {
height: var(--sp-xxs); height: var(--sp-xxs);
width: 94%; width: 94%;
@@ -519,44 +514,3 @@
@include t.use-typography("body-small"); @include t.use-typography("body-small");
color: var(--color-accent-tertiary); 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.common.data.macros :as dm]
[app.config :as cf] [app.config :as cf]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.nitrate :as dnt]
[app.main.router :as rt] [app.main.router :as rt]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]] [app.main.ui.components.dropdown-menu :refer [dropdown-menu-item*]]
@@ -116,26 +115,6 @@
:has-dropdown false :has-dropdown false
:is-highlighted false}])))) :is-highlighted false}]))))
(mf/defc nitrate-sidebar*
[]
(let [handle-click
(mf/use-fn
(fn []
(st/emit! (dnt/show-nitrate-popup))))]
;; 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* (mf/defc team*
[{:keys [is-owner team]}] [{:keys [is-owner team]}]
(let [subscription (:subscription team) (let [subscription (:subscription team)

View File

@@ -205,28 +205,3 @@
overflow-wrap: break-word; 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-36: px2rem(36);
$sz-40: px2rem(40); $sz-40: px2rem(40);
$sz-48: px2rem(48); $sz-48: px2rem(48);
$sz-64: px2rem(64);
$sz-88: px2rem(88); $sz-88: px2rem(88);
$sz-96: px2rem(96); $sz-96: px2rem(96);
$sz-120: px2rem(120); $sz-120: px2rem(120);

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,6 @@
(:require (:require
[app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.select :refer [select*]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.forms :as fm] [app.util.forms :as fm]
[app.util.keyboard :as k] [app.util.keyboard :as k]
@@ -48,23 +47,6 @@
[:> input* props])) [:> 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* (mf/defc form-submit*
[{:keys [disabled on-submit] :rest props}] [{:keys [disabled on-submit] :rest props}]
(let [form (mf/use-ctx context) (let [form (mf/use-ctx context)
@@ -97,4 +79,4 @@
(when (fn? on-submit) (when (fn? on-submit)
(on-submit form event))))] (on-submit form event))))]
[:> (mf/provider context) {:value form} [:> (mf/provider context) {:value form}
[:form {:class class :on-submit on-submit'} children]])) [:form {:class class :on-submit on-submit'} children]]))

View File

@@ -7,85 +7,42 @@
(ns app.main.ui.nitrate.nitrate-form (ns app.main.ui.nitrate.nitrate-form
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.schema :as sm]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.ui.components.forms :as fm]
[app.main.ui.ds.buttons.button :refer [button*]] [app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.icons :as deprecated-icon]
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.util.dom :as dom] [app.util.dom :as dom]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(def ^:private schema:nitrate-form ;; FIXME: rename to `form` (remove the nitrate prefix from namespace,
[:map {:title "NitrateForm"} ;; because it is already under nitrate)
[:subscription [::sm/one-of #{:monthly :yearly}]]])
(mf/defc nitrate-form-modal* (mf/defc nitrate-form-modal*
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :nitrate-form ::mf/register-as :nitrate-form}
::mf/wrap-props true} []
[connectivity] (let [on-click
(let [show-buttons (:licenses connectivity)
initial (mf/with-memo []
{:subscription "yearly"})
form (fm/use-form :schema schema:nitrate-form
:initial initial)
on-click
(mf/use-fn (mf/use-fn
(fn [] (fn []
;; TODO Start licenses with selected type
(dom/open-new-window "/control-center/licenses/start")))] (dom/open-new-window "/control-center/licenses/start")))]
[:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :subscription-success)} [:div {:class (stl/css :modal-container)}
[:button {:class (stl/css :close-btn) :on-click modal/hide!} [:div {:class (stl/css :nitrate-form)}
[:> icon* {:icon-id "close"
:size "m"}]]
[:div {:class (stl/css :modal-success-content)}
[:div {:class (stl/css :modal-start)}
;; TODO this svg is a placeholder. Use the proper one when created
[:> raw-svg* {:id "logo-subscription"}]]
[:div {:class (stl/css :modal-end)} [:div {:class (stl/css :modal-header)}
[:div {:class (stl/css :modal-title)} [:h2 {:class (stl/css :modal-title)}
"Unlock Nitrate Features"] "BUY NITRATE"]
[:p {:class (stl/css :modal-text-large)} [:button {:class (stl/css :modal-close-btn)
"Prow scuttle parrel provost."] :on-click modal/hide!} deprecated-icon/close]]
[:p {:class (stl/css :modal-text-large)}
"Sail ho shrouds spirits boom mizzenmast yardarm. Pinnace holystone mizzenmast quarter crow's nest nipperkin grog yardarm hempen halter furl."]
[:p {:class (stl/css :modal-text-large)}
"Deadlights jack lad schooner scallywag dance the hempen jig carouser broadside cable strike colors."]
(if show-buttons
[:& fm/form {:form form}
[:p {:class (stl/css :modal-text-large)}
[:& fm/radio-buttons [:div {:class (stl/css :modal-content)}
{:options [{:label "Price Tag Montly" :value "monthly"} "Nitrate is so cool! You should buy it!"]
{:label "Price Tag Yearly (Discount)" :value "yearly"}]
:name :subscription
:class (stl/css :radio-btns)}]]
[:p {:class (stl/css :modal-text-large :modal-buttons-section)} [:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :modal-buttons-section)} [:div {:class (stl/css :action-buttons)}
[:> button* {:variant "primary" [:> button* {:variant "primary"
:on-click on-click :on-click on-click}
:class (stl/css :modal-button)} "BUY NOW!"]]]]]]))
"UPGRADE TO NITRATE"]
[:div {:class (stl/css :modal-text-small :modal-info)}
"Cancel anytime before your next billing cycle."]]]
[:p {:class (stl/css :modal-text-medium)}
[:a {:class (stl/css :link)}
"See my current plan"]]]
[:div {:class (stl/css :contact)}
[:p {:class (stl/css :modal-text-large)}
"Contact us to upgrade to Nitrate:"]
[:p {:class (stl/css :modal-text-large)}
[:a {:class (stl/css :link) :href "mailto:sales@penpot.app"}
"sales@penpot.app"]]])]]]]))

View File

@@ -5,92 +5,48 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "ds/typography.scss" as t;
@use "ds/_borders.scss" as *;
@use "ds/spacing.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_utils.scss" as *;
.modal-overlay { .modal-overlay {
@extend .modal-overlay-base; @extend .modal-overlay-base;
z-index: var(--z-index-notifications);
} }
.modal-dialog { .modal-container {
@extend .modal-container-base; @extend .modal-container-base;
max-block-size: initial;
min-inline-size: px2rem(648);
} }
.close-btn { .modal-header {
@extend .modal-close-btn-base; margin-bottom: deprecated.$s-24;
} }
.modal-title { .modal-title {
@include t.use-typography("title-large"); @include deprecated.uppercaseTitleTipography;
margin-block-end: var(--sp-xxxl);
color: var(--modal-title-foreground-color); color: var(--modal-title-foreground-color);
display: flex;
gap: var(--sp-m);
} }
.modal-text-large { .modal-close-btn {
@include t.use-typography("body-large"); @extend .modal-close-btn-base;
} }
.modal-text-medium { .modal-content {
@include t.use-typography("body-medium"); margin-bottom: deprecated.$s-24;
} }
.modal-text-small { .nitrate-form {
@include t.use-typography("body-small"); min-width: deprecated.$s-400;
} }
.modal-info { .action-buttons {
margin-block-start: var(--sp-s); @extend .modal-action-btns;
width: 40%;
} }
.modal-content, .cancel-button {
.modal-end { @extend .modal-cancel-btn;
color: var(--color-foreground-secondary);
display: flex;
flex-direction: column;
} }
.modal-success-content { .accept-btn {
display: flex; @extend .modal-accept-btn;
gap: $sz-40;
}
.modal-start { &.danger {
display: flex; @extend .modal-danger-btn;
justify-content: center;
max-inline-size: $sz-224;
svg {
inline-size: 100%;
block-size: auto;
}
@media (max-inline-size: 992px) {
display: none;
} }
} }
.radio-btns {
label {
@include t.use-typography("body-large");
padding: 0;
}
display: flex;
flex-direction: column;
padding: var(--sp-l) 0 0 0;
gap: 0;
}
.contact {
margin-block-start: $sz-96;
color: var(--color-foreground-primary);
}

View File

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

View File

@@ -13,10 +13,10 @@
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.modal :refer [modal-container*]] [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.change-email]
[app.main.ui.settings.delete-account] [app.main.ui.settings.delete-account]
[app.main.ui.settings.feedback :refer [feedback-page*]] [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.notifications :refer [notifications-page*]]
[app.main.ui.settings.options :refer [options-page]] [app.main.ui.settings.options :refer [options-page]]
[app.main.ui.settings.password :refer [password-page]] [app.main.ui.settings.password :refer [password-page]]
@@ -73,8 +73,8 @@
:settings-subscription :settings-subscription
[:> subscription-page* {:profile profile}] [:> subscription-page* {:profile profile}]
:settings-integrations :settings-access-tokens
[:> integrations-page*] [:& access-tokens-page]
:settings-notifications :settings-notifications
[:& notifications-page* {:profile profile}])]]]])) [:& 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 (def ^:private go-settings-subscription
#(st/emit! (rt/nav :settings-subscription))) #(st/emit! (rt/nav :settings-subscription)))
(def ^:private go-settings-integrations (def ^:private go-settings-access-tokens
#(st/emit! (rt/nav :settings-integrations))) #(st/emit! (rt/nav :settings-access-tokens)))
(def ^:private go-settings-notifications (def ^:private go-settings-notifications
#(st/emit! (rt/nav :settings-notifications))) #(st/emit! (rt/nav :settings-notifications)))
@@ -66,7 +66,7 @@
options? (= section :settings-options) options? (= section :settings-options)
feedback? (= section :settings-feedback) feedback? (= section :settings-feedback)
subscription? (= section :settings-subscription) subscription? (= section :settings-subscription)
integrations? (= section :settings-integrations) access-tokens? (= section :settings-access-tokens)
notifications? (= section :settings-notifications) notifications? (= section :settings-notifications)
team-id (or (dtm/get-last-team-id) team-id (or (dtm/get-last-team-id)
(:default-team-id profile)) (:default-team-id profile))
@@ -115,13 +115,12 @@
:data-testid "settings-subscription"} :data-testid "settings-subscription"}
[:span {:class (stl/css :element-title)} (tr "subscription.labels")]]) [:span {:class (stl/css :element-title)} (tr "subscription.labels")]])
(when (or (contains? cf/flags :access-tokens) (when (contains? cf/flags :access-tokens)
(contains? cf/flags :mcp)) [:li {:class (stl/css-case :current access-tokens?
[:li {:class (stl/css-case :current integrations?
:settings-item true) :settings-item true)
:on-click go-settings-integrations :on-click go-settings-access-tokens
:data-testid "settings-integrations"} :data-testid "settings-access-tokens"}
[:span {:class (stl/css :element-title)} (tr "labels.integrations")]]) [:span {:class (stl/css :element-title)} (tr "labels.access-tokens")]])
[:hr {:class (stl/css :sidebar-separator)}] [:hr {:class (stl/css :sidebar-separator)}]

View File

@@ -4,7 +4,6 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf]
[app.main.data.auth :as da] [app.main.data.auth :as da]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
@@ -563,7 +562,7 @@
:recommended (= subscription-type "professional") :recommended (= subscription-type "professional")
:show-button-cta (= subscription-type "professional")}]) :show-button-cta (= subscription-type "professional")}])
(when (and (not= subscription-type "enterprise") (not (contains? cf/flags :nitrate))) (when (not= subscription-type "enterprise")
[:> plan-card* {:card-title (tr "subscription.settings.enterprise") [:> plan-card* {:card-title (tr "subscription.settings.enterprise")
:card-title-icon i/character-e :card-title-icon i/character-e
:price-value "$950" :price-value "$950"
@@ -576,21 +575,5 @@
:cta-link #(open-subscription-modal "enterprise" subscription) :cta-link #(open-subscription-modal "enterprise" subscription)
:cta-text-with-icon (tr "subscription.settings.more-information") :cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page :cta-link-with-icon go-to-pricing-page
:show-button-cta (= subscription-type "professional")}]) :show-button-cta (= subscription-type "professional")}])]]]))
;; TODO add translations for this texts when we have the definitive ones
(when (and (contains? cf/flags :nitrate) (not (:nitrate-licence profile)))
[:> plan-card* {:card-title "Business Nitrate"
:card-title-icon i/character-n
:price-value "$25"
:price-period "org member"
:benefits-title (tr "subscription.settings.benefits.all-unlimited-benefits")
:benefits ["Crea organizaciones y añade personas, que usarán Penpot con las reglas que configures."
"Acceso exclusivo al Control Center"
"Lorem ipsum"]
:cta-text (tr "subscription.settings.subscribe")
;; TODO add link to open nitrate modal
:cta-link #(dom/open-new-window "https://penpot.app/nitrate")
:cta-text-with-icon (tr "subscription.settings.more-information")
:cta-link-with-icon go-to-pricing-page}])]]]))

View File

@@ -50,7 +50,7 @@
(mf/defc workspace-content* (mf/defc workspace-content*
{::mf/private true} {::mf/private true}
[{:keys [file layout page wglobal file-version-id]}] [{:keys [file layout page wglobal]}]
(let [palete-size (mf/use-state nil) (let [palete-size (mf/use-state nil)
selected (mf/deref refs/selected-shapes) selected (mf/deref refs/selected-shapes)
@@ -109,7 +109,6 @@
:wglobal wglobal :wglobal wglobal
:selected selected :selected selected
:layout layout :layout layout
:file-version-id file-version-id
:palete-size :palete-size
(when (and (or colorpalette? textpalette?) (not hide-ui?)) (when (and (or colorpalette? textpalette?) (not hide-ui?))
@palete-size)}]]] @palete-size)}]]]
@@ -169,7 +168,7 @@
(mf/defc workspace-inner* (mf/defc workspace-inner*
{::mf/private true} {::mf/private true}
[{:keys [page-id file-id file layout wglobal file-version-id]}] [{:keys [page-id file-id file layout wglobal]}]
(let [page-ref (mf/with-memo [file-id page-id] (let [page-ref (mf/with-memo [file-id page-id]
(make-page-ref file-id page-id)) (make-page-ref file-id page-id))
page (mf/deref page-ref)] page (mf/deref page-ref)]
@@ -188,8 +187,7 @@
[:> workspace-content* {:file file [:> workspace-content* {:file file
:page page :page page
:wglobal wglobal :wglobal wglobal
:layout layout :layout layout}]
:file-version-id file-version-id}]
[:> workspace-loader*]))) [:> workspace-loader*])))
(mf/defc workspace* (mf/defc workspace*
@@ -201,7 +199,6 @@
layout (mf/deref refs/workspace-layout) layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global) wglobal (mf/deref refs/workspace-global)
file-version-id (mf/deref refs/workspace-file-version-id)
team-ref (mf/with-memo [team-id] team-ref (mf/with-memo [team-id]
(make-team-ref team-id)) (make-team-ref team-id))
@@ -277,8 +274,7 @@
:file-id file-id :file-id file-id
:file file :file file
:wglobal wglobal :wglobal wglobal
:layout layout :layout layout}])
:file-version-id file-version-id}])
(when (or (not (and file-loaded? page-id)) (when (or (not (and file-loaded? page-id))
;; in wasm renderer, extend the pixel loader until the first frame is rendered ;; in wasm renderer, extend the pixel loader until the first frame is rendered
;; but do not apply it when switching pages ;; but do not apply it when switching pages

View File

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

View File

@@ -84,8 +84,6 @@
:on-click on-select-shape :on-click on-select-shape
:on-context-menu on-context-menu :on-context-menu on-context-menu
:data-testid "layer-row" :data-testid "layer-row"
:role "checkbox"
:aria-checked selected?
:class (stl/css-case :class (stl/css-case
:layer-row true :layer-row true
:highlight highlighted? :highlight highlighted?

View File

@@ -73,7 +73,7 @@
objects))) objects)))
(mf/defc viewport* (mf/defc viewport*
[{:keys [selected wglobal wlocal layout file page palete-size file-version-id]}] [{:keys [selected wglobal wlocal layout file page palete-size]}]
(let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check (let [;; When adding data from workspace-local revisit `app.main.ui.workspace` to check
;; that the new parameter is sent ;; that the new parameter is sent
{:keys [edit-path {:keys [edit-path
@@ -141,7 +141,6 @@
canvas-ref (mf/use-ref nil) canvas-ref (mf/use-ref nil)
text-editor-ref (mf/use-ref nil) text-editor-ref (mf/use-ref nil)
last-file-version-id-ref (mf/use-ref nil)
;; STATE REFS ;; STATE REFS
disable-paste-ref (mf/use-ref false) disable-paste-ref (mf/use-ref false)
@@ -345,18 +344,11 @@
(when (and @canvas-init? preview-blend) (when (and @canvas-init? preview-blend)
(wasm.api/request-render "with-effect"))) (wasm.api/request-render "with-effect")))
(mf/with-effect [@canvas-init? file-version-id zoom vbox background] (mf/with-effect [@canvas-init? zoom vbox background]
(when @canvas-init? (when (and @canvas-init? (not @initialized?))
(if (not @initialized?) (wasm.api/clear-canvas-pixels)
(do (wasm.api/initialize-viewport base-objects zoom vbox background)
(wasm.api/clear-canvas-pixels) (reset! initialized? true)))
(wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true)
(mf/set-ref-val! last-file-version-id-ref file-version-id))
(when (and (some? file-version-id)
(not= file-version-id (mf/ref-val last-file-version-id-ref)))
(wasm.api/initialize-viewport base-objects zoom vbox background)
(mf/set-ref-val! last-file-version-id-ref file-version-id)))))
(mf/with-effect [focus] (mf/with-effect [focus]
(when (and @canvas-init? @initialized?) (when (and @canvas-init? @initialized?)

View File

@@ -261,39 +261,7 @@
:else :else
(let [child-id (obj/get child "$id")] (let [child-id (obj/get child "$id")]
(st/emit! (dwt/move-shapes-to-frame #{child-id} id nil nil) (st/emit! (dwt/move-shapes-to-frame #{child-id} id nil nil)
(ptk/data-event :layout/update {:ids [id]}))))) (ptk/data-event :layout/update {:ids [id]})))))))
:horizontalSizing
{:this true
:get #(-> % u/proxy->shape :layout-item-h-sizing (d/nilv :fix) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/item-h-sizing-types value))
(u/display-not-valid :horizontalPadding value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :horizontalPadding "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-h-sizing value})))))}
:verticalSizing
{:this true
:get #(-> % u/proxy->shape :layout-item-v-sizing (d/nilv :fix) d/name)
:set
(fn [_ value]
(let [value (keyword value)]
(cond
(not (contains? ctl/item-v-sizing-types value))
(u/display-not-valid :verticalSizing value)
(not (r/check-permission plugin-id "content:write"))
(u/display-not-valid :verticalSizing "Plugin doesn't have 'content:write' permission")
:else
(st/emit! (dwsl/update-layout-child #{id} {:layout-item-v-sizing value})))))}))
(defn layout-child-proxy? [p] (defn layout-child-proxy? [p]

View File

@@ -598,10 +598,3 @@
(case axis (case axis
:y "horizontal" :y "horizontal"
:x "vertical")) :x "vertical"))
(defn format-geom-rect
[{:keys [x y width height]}]
#js {:x x
:y y
:width width
:height height})

View File

@@ -17,10 +17,6 @@
[app.util.object :as obj] [app.util.object :as obj]
[beicon.v2.core :as rx])) [beicon.v2.core :as rx]))
;; Needs to be here because moving it to `app.main.data.workspace.mcp` will
;; cause a circular dependency
(def mcp-plugin-id "96dfa740-005d-8020-8007-55ede24a2bae")
;; Stores the installed plugins information ;; Stores the installed plugins information
(defonce ^:private registry (atom {})) (defonce ^:private registry (atom {}))
@@ -131,6 +127,5 @@
(defn check-permission (defn check-permission
[plugin-id permission] [plugin-id permission]
(or (= plugin-id "00000000-0000-0000-0000-000000000000") (or (= plugin-id "00000000-0000-0000-0000-000000000000")
(= plugin-id mcp-plugin-id)
(let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])] (let [{:keys [permissions]} (dm/get-in @registry [:data plugin-id])]
(contains? permissions permission)))) (contains? permissions permission))))

View File

@@ -8,7 +8,6 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.geom.shapes.text :as gst]
[app.common.record :as crc] [app.common.record :as crc]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.types.shape :as cts] [app.common.types.shape :as cts]
@@ -652,7 +651,4 @@
(u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission") (u/display-not-valid :verticalAlign "Plugin doesn't have 'content:write' permission")
:else :else
(st/emit! (dwt/update-attrs id {:vertical-align value})))))} (st/emit! (dwt/update-attrs id {:vertical-align value})))))}))
{:name "textBounds"
:get #(-> % u/proxy->shape gst/shape->bounds format/format-geom-rect)}))

View File

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

View File

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

View File

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

View File

@@ -642,15 +642,13 @@ export class SelectionController extends EventTarget {
} else { } else {
this.#anchorNode = anchorNode; this.#anchorNode = anchorNode;
this.#anchorOffset = anchorOffset; this.#anchorOffset = anchorOffset;
this.#focusNode = focusNode; if (anchorNode === focusNode) {
this.#focusOffset = focusOffset; this.#focusNode = this.#anchorNode;
// setPosition() collapses the selection to a single caret. We must only use it this.#focusOffset = this.#anchorOffset;
// when anchorOffset === focusOffset. When both points are in the same node but
// offsets differ (e.g. selecting "hola" in "hola adios"), we need setBaseAndExtent()
// to preserve the range so we don't incorrectly collapse ranges and lose the selection.
if (anchorNode === focusNode && anchorOffset === focusOffset) {
this.#selection.setPosition(anchorNode, anchorOffset); this.#selection.setPosition(anchorNode, anchorOffset);
} else { } else {
this.#focusNode = focusNode;
this.#focusOffset = focusOffset;
this.#selection.setBaseAndExtent( this.#selection.setBaseAndExtent(
anchorNode, anchorNode,
anchorOffset, anchorOffset,

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