Compare commits

..

1 Commits

Author SHA1 Message Date
Alejandro Alonso
782adec4e6 🐛 Fix stroke artifacts 2026-02-23 09:34:47 +01:00
202 changed files with 2063 additions and 9496 deletions

View File

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

View File

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

View File

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

View File

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

2
.gitignore vendored
View File

@@ -69,7 +69,7 @@
/frontend/test-results/
/other/
/scripts/
/nexus/
/telemetry/
/tmp/
/vendor/**/target
/vendor/svgclean/bundle*.js

View File

@@ -1,32 +1,7 @@
# CHANGELOG
## 2.15.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Allow duplicating color and typography styles (by @MkDev11) [Github #2912](https://github.com/penpot/penpot/issues/2912)
- 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)
- Import Tokens from linked library [Github #8391](https://github.com/penpot/penpot/pull/8391)
### :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)
### :boom: Breaking changes & Deprecations
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
### :sparkles: New features & Enhancements
- Access to design tokens in Penpot Plugins [Taiga #8990](https://tree.taiga.io/project/penpot/us/8990)
@@ -53,8 +28,6 @@
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
- Fix 45 rotated board titles rendered incorrectly [Taiga #13306](https://tree.taiga.io/project/penpot/issue/13306)
## 2.13.3

View File

@@ -28,8 +28,8 @@
com.google.guava/guava {:mvn/version "33.4.8-jre"}
funcool/yetti
{:git/tag "v11.9"
:git/sha "5fad7a9"
{:git/tag "v11.8"
:git/sha "1d1b33f"
:git/url "https://github.com/funcool/yetti.git"
:exclusions [org.slf4j/slf4j-api]}

View File

@@ -2,7 +2,6 @@
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
export PENPOT_NEXUS_SHARED_KEY=super-secret-nexus-api-key
export PENPOT_SECRET_KEY=super-secret-devenv-key
# DEPRECATED: only used for subscriptions

View File

@@ -98,12 +98,12 @@
[:http-server-port {:optional true} ::sm/int]
[:http-server-host {:optional true} :string]
[:http-server-max-body-size {:optional true} ::sm/int]
[:http-server-max-multipart-body-size {:optional true} ::sm/int]
[:http-server-io-threads {:optional true} ::sm/int]
[:http-server-max-worker-threads {:optional true} ::sm/int]
[:exporter-shared-key {:optional true} :string]
[:nitrate-shared-key {:optional true} :string]
[:nexus-shared-key {:optional true} :string]
[:management-api-key {:optional true} :string]
[:telemetry-uri {:optional true} :string]

View File

@@ -42,8 +42,8 @@
(def default-params
{::port 6060
::host "0.0.0.0"
::max-body-size 367001600 ; default 350 MiB
})
::max-body-size 31457280 ; default 30 MiB
::max-multipart-body-size 367001600}) ; default 350 MiB
(defmethod ig/expand-key ::server
[k v]
@@ -56,6 +56,7 @@
[::io-threads {:optional true} ::sm/int]
[::max-worker-threads {:optional true} ::sm/int]
[::max-body-size {:optional true} ::sm/int]
[::max-multipart-body-size {:optional true} ::sm/int]
[::router {:optional true} [:fn r/router?]]
[::handler {:optional true} ::sm/fn]])
@@ -78,7 +79,7 @@
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:xnio/direct-buffers false
:xnio/io-threads (::io-threads cfg)
:xnio/max-worker-threads (::max-worker-threads cfg)

View File

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

View File

@@ -120,7 +120,7 @@
;; an external storage and data cleared.
(def ^:private schema:event
[:map {:title "AuditEvent"}
[:map {:title "event"}
[::type ::sm/text]
[::name ::sm/text]
[::profile-id ::sm/uuid]

View File

@@ -10,11 +10,14 @@
[app.common.logging :as l]
[app.common.schema :as sm]
[app.common.transit :as t]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.db :as db]
[app.http.client :as http]
[app.setup :as-alias setup]
[app.tokens :as tokens]
[integrant.core :as ig]
[lambdaisland.uri :as u]
[promesa.exec :as px]))
;; This is a task responsible to send the accumulated events to
@@ -49,18 +52,19 @@
(defn- send!
[{:keys [::uri] :as cfg} events]
(let [skey (-> cfg ::setup/shared-keys :nexus)
(let [token (tokens/generate cfg
{:iss "authentication"
:uid uuid/zero})
body (t/encode {:events events})
headers {"content-type" "application/transit+json"
"origin" (str (cf/get :public-uri))
"x-shared-key" (str "nexus " skey)}
"cookie" (u/map->query-string {:auth-token token})}
params {:uri uri
:timeout 12000
:method :post
:headers headers
:body body}
resp (http/req! cfg params)]
(if (= (:status resp) 204)
true
(do
@@ -105,7 +109,7 @@
(def ^:private schema:handler-params
[:map
::db/pool
::setup/shared-keys
::setup/props
::http/client])
(defmethod ig/assert-key ::handler

View File

@@ -226,10 +226,11 @@
::http/server
{::http/port (cf/get :http-server-port)
::http/host (cf/get :http-server-host)
::http/router (ig/ref ::http/router)
::http/io-threads (cf/get :http-server-io-threads)
::http/max-worker-threads (cf/get :http-server-max-worker-threads)
::http/max-body-size (cf/get :http-server-max-body-size)
::http/router (ig/ref ::http/router)
::http/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::mtx/metrics (ig/ref ::mtx/metrics)}
::ldap/provider
@@ -466,17 +467,16 @@
::setup/shared-keys
{::setup/props (ig/ref ::setup/props)
:nexus (cf/get :nexus-shared-key)
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
:nitrate (cf/get :nitrate-shared-key)
:exporter (cf/get :exporter-shared-key)}
::setup/clock
{}
:app.loggers.audit.archive-task/handler
{::setup/shared-keys (ig/ref ::setup/shared-keys)
::http.client/client (ig/ref ::http.client/client)
::db/pool (ig/ref ::db/pool)}
{::setup/props (ig/ref ::setup/props)
::db/pool (ig/ref ::db/pool)
::http.client/client (ig/ref ::http.client/client)}
:app.loggers.audit.gc-task/handler
{::db/pool (ig/ref ::db/pool)}

View File

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

View File

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

View File

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

View File

@@ -87,10 +87,6 @@
[:map
[:valid ::sm/boolean]])
(def ^:private schema:connectivity
[:map
[:licenses ::sm/boolean]])
(defn- get-team-org
[cfg {:keys [team-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
@@ -101,11 +97,6 @@
(let [baseuri (cf/get :nitrate-backend-uri)]
(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
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@@ -114,8 +105,7 @@
[_ cfg]
(when (contains? cf/flags :nitrate)
{:get-team-org (partial get-team-org cfg)
:is-valid-user (partial is-valid-user cfg)
:connectivity (partial get-connectivity cfg)}))
:is-valid-user (partial is-valid-user cfg)}))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; UTILS
@@ -126,8 +116,7 @@
[cfg profile]
(try
(let [nitrate-licence (call cfg :is-valid-user {:profile-id (:id profile)})]
(assoc-in profile [:props :nitrate-license]
(select-keys nitrate-licence [:valid :created-at])))
(assoc profile :nitrate-licence (:valid nitrate-licence)))
(catch Throwable cause
(l/error :hint "failed to get nitrate licence"
:profile-id (:id profile)
@@ -139,7 +128,3 @@
(let [params (assoc (or params {}) :team-id (:id team))
org (call cfg :get-team-org params)]
(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)
204
200))
headers (::http/headers mdata {})
headers (cond-> headers
(and (yres/stream-body? result)
(not (contains? headers "content-type")))
headers (cond-> (::http/headers mdata {})
(yres/stream-body? result)
(assoc "content-type" "application/octet-stream"))]
{::yres/status status
::yres/headers headers
::yres/body result}))]
@@ -262,7 +258,6 @@
'app.rpc.commands.ldap
'app.rpc.commands.management
'app.rpc.commands.media
'app.rpc.commands.nitrate
'app.rpc.commands.profile
'app.rpc.commands.projects
'app.rpc.commands.search

View File

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

View File

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

View File

@@ -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
[:map {:title "ProfileProps"}
[:plugins {:optional true} schema:plugin-registry]
[:mcp-status {:optional true} ::sm/boolean]
[:newsletter-updates {:optional true} ::sm/boolean]
[:newsletter-news {:optional true} ::sm/boolean]
[:onboarding-team-id {:optional true} ::sm/uuid]

View File

@@ -82,37 +82,45 @@
(db/tx-run! cfg (fn [{:keys [::db/conn]}]
(db/xact-lock! conn 0)
(when-not key
(l/wrn :hint (str "using autogenerated secret-key, it will change "
"on each restart and will invalidate "
"all sessions on each restart, it is highly "
"recommended setting up the "
"PENPOT_SECRET_KEY environment variable")))
(l/warn :hint (str "using autogenerated secret-key, it will change on each restart and will invalidate "
"all sessions on each restart, it is highly recommended setting up the "
"PENPOT_SECRET_KEY environment variable")))
(let [secret (or key (generate-random-key))]
(-> (get-all-props conn)
(assoc :secret-key secret)
(assoc :tokens-key (keys/derive secret :salt "tokens"))
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
(sm/register! ::props [:map-of :keyword ::sm/any])
(defmethod ig/init-key ::shared-keys
[_ {:keys [::props] :as cfg}]
(let [secret (get props :secret-key)]
(reduce (fn [keys id]
(let [key (or (get cfg id)
(-> (keys/derive secret :salt (name id))
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :id (name id) :hint "key is disabled because empty string found")
keys)
(do
(l/inf :id (name id) :hint "key initialized" :key (d/obfuscate-string key))
(assoc keys id key)))))
{}
[:exporter
:nitrate
:nexus])))
(d/without-nils
{:exporter
(let [key (or (get cfg :exporter)
(-> (keys/derive secret :salt "exporter")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "exporter key is disabled because empty string found")
nil)
(do
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
key)))
(sm/register! ::props [:map-of :keyword ::sm/any])
(sm/register! ::shared-keys [:map-of :keyword ::sm/text])
:nitrate
(let [key (or (get cfg :nitrate)
(-> (keys/derive secret :salt "nitrate")
(bc/bytes->b64-str true)))]
(if (or (str/empty? key)
(str/blank? key))
(do
(l/wrn :hint "nitrate key is disabled because empty string found")
nil)
(do
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
key)))})))

View File

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

View File

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

View File

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

View File

Binary file not shown.

View File

@@ -760,21 +760,6 @@
default
v))))
(defn percent?
[v]
(str/numeric? (str/rtrim v "%")))
(defn parse-percent
([v]
(parse-percent v nil))
([v default]
(if (str/ends-with? v "%")
(let [v (impl-parse-double (str/trim v "%"))]
(if (or (nil? v) (nan? v))
default
(/ v 100)))
(parse-double v default))))
(defn parse-uuid
[v]
(try

View File

@@ -31,56 +31,18 @@
(def schema:token-value-generic
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-numeric
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.invalid-value" (:value %))}
(fn [value]
(if (str/numeric? value)
(let [n (d/parse-double value)]
(some? n))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-percent
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.value-with-percent" (:value %))}
(fn [value]
(if (d/percent? value)
(let [v (d/parse-percent value)]
(some? v))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-composite-ref
[::sm/text {:error/fn token-value-empty-fn}])
(def schema:token-value-opacity
[:and
[::sm/text {:error/fn token-value-empty-fn}]
[:fn {:error/fn #(tr "workspace.tokens.opacity-range")}
(fn [opacity]
(if (str/numeric? opacity)
(let [n (d/parse-percent opacity)]
(and (some? n) (<= 0 n 1)))
true))]]) ;; Leave references or formulas to be checked by the resolver
(def schema:token-value-font-family
[:or
[:vector ::sm/text]
cto/schema:token-ref])
(def schema:token-value-font-weight
[:or
[:fn {:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value")}
cto/valid-font-weight-variant]
::sm/text]) ;; Leave references or formulas to be checked by the resolver
[:vector ::sm/text])
(def schema:token-value-typography-map
[:map
[:font-family {:optional true} schema:token-value-font-family]
[:font-size {:optional true} schema:token-value-numeric]
[:font-weight {:optional true} schema:token-value-font-weight]
[:line-height {:optional true} schema:token-value-percent]
[:font-weight {:optional true} schema:token-value-generic]
[:font-size {:optional true} schema:token-value-generic]
[:line-height {:optional true} schema:token-value-generic]
[:letter-spacing {:optional true} schema:token-value-generic]
[:paragraph-spacing {:optional true} schema:token-value-generic]
[:text-decoration {:optional true} schema:token-value-generic]
@@ -122,10 +84,7 @@
[token-type]
[:multi {:dispatch (constantly token-type)
:title "Token Value"}
[:opacity schema:token-value-opacity]
[:font-family schema:token-value-font-family]
[:font-size schema:token-value-numeric]
[:font-weight schema:token-value-font-weight]
[:typography schema:token-value-typography]
[:shadow schema:token-value-shadow]
[::m/default schema:token-value-generic]])
@@ -210,7 +169,7 @@
[tokens-lib set-id]
[:and
[:string {:min 1 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-set-already-exists")}
[:fn {:error/fn #(tr "errors.token-set-already-exists" (:value %))}
(fn [name]
(or (nil? tokens-lib)
(let [set (ctob/get-set-by-name tokens-lib name)]
@@ -237,7 +196,7 @@
[tokens-lib name theme-id]
[:and
[:string {:min 0 :max 255 :error/fn #(str (:value %) (tr "workspace.tokens.token-name-length-validation-error"))}]
[:fn {:error/fn #(tr "errors.token-theme-already-exists")}
[:fn {:error/fn #(tr "errors.token-theme-already-exists" (:value %))}
(fn [group]
(or (nil? tokens-lib)
(let [theme (ctob/get-theme-by-name tokens-lib group name)]

View File

@@ -119,13 +119,12 @@
:strict-session-cookies
:telemetry
:terms-and-privacy-checkbox
;; Only for developtment.
:tiered-file-data-storage
:token-base-font-size
:token-color
:token-shadow
:token-tokenscript
:token-import-from-library
;; Only for developtment.
:transit-readable-response
:user-feedback
;; TODO: remove this flag.
@@ -153,9 +152,7 @@
:redis-cache
;; Activates the nitrate module
:nitrate
:mcp})
:nitrate})
(def all-flags
(set/union email login varia))
@@ -181,8 +178,7 @@
:enable-token-color
:enable-token-shadow
:enable-inspect-styles
:enable-feature-fdata-objects-map
:enable-token-import-from-library])
:enable-feature-fdata-objects-map])
(defn parse
[& flags]

View File

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

View File

@@ -5,7 +5,7 @@
;; Copyright (c) KALEIDOS INC
(ns app.common.schema
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys select-keys])
(:refer-clojure :exclude [deref merge parse-uuid parse-long parse-double parse-boolean type keys])
#?(:cljs (:require-macros [app.common.schema :refer [ignoring]]))
(:require
#?(:clj [malli.dev.pretty :as mdp])
@@ -93,11 +93,6 @@
[& items]
(apply mu/merge (map schema items)))
(defn select-keys
[s keys & {:as opts}]
(let [s (schema s)]
(mu/select-keys s keys opts)))
(defn assoc-key
"Add a key & value to a schema of type [:map]. If the first level node of the schema
is not a map, will do a depth search to find the first map node and add the key there."
@@ -143,10 +138,10 @@
(mu/optional-keys schema keys default-options)))
(defn required-keys
([s]
(mu/required-keys (schema s) nil default-options))
([s keys]
(mu/required-keys (schema s) keys default-options)))
([schema]
(mu/required-keys schema nil default-options))
([schema keys]
(mu/required-keys schema keys default-options)))
(defn transformer
[& transformers]
@@ -651,7 +646,7 @@
{:title "set"
:description "Set of Strings"
:error/message "should be a set of strings"
:gen/gen (sg/mcat (fn [_] (sg/generator kind)) sg/int)
:gen/gen (-> kind sg/generator sg/set)
:decode/string decode
:decode/json decode
:encode/string encode-string

View File

@@ -28,7 +28,6 @@
["date-fns/locale/eu$default" :as dfn-eu]
["date-fns/locale/fa-IR$default" :as dfn-fa-ir]
["date-fns/locale/fr$default" :as dfn-fr]
["date-fns/locale/fr-CA$default" :as dfn-fr-ca]
["date-fns/locale/gl$default" :as dfn-gl]
["date-fns/locale/he$default" :as dfn-he]
["date-fns/locale/hr$default" :as dfn-hr]
@@ -253,7 +252,6 @@
:fa dfn-fa-ir
:fa_ir dfn-fa-ir
:fr dfn-fr
:fr_ca dfn-fr-ca
:he dfn-he
:pt dfn-pt
:pt_pt dfn-pt

View File

@@ -115,25 +115,21 @@
(defn get-frames
"Retrieves all frame objects as vector"
([objects] (get-frames objects nil))
([objects {:keys [skip-components? skip-copies? ignore-index?]
([objects {:keys [skip-components? skip-copies?]
:or {skip-components? false
skip-copies? false
ignore-index? false}}]
(let [frame-index
(if (and (not ignore-index?) (-> objects meta ::index-frames))
(-> objects meta ::index-frames)
(let [lookup (d/getf objects)
xform (comp (remove #(= uuid/zero %))
(keep lookup)
(filter cfh/frame-shape?))]
(->> (keys objects)
(sequence xform))))]
(->> frame-index
(remove #(or (and ^boolean skip-components?
^boolean (ctk/instance-head? %))
(and ^boolean skip-copies?
(and ^boolean (ctk/instance-head? %)
(not ^boolean (ctk/main-instance? %))))))))))
skip-copies? false}}]
(->> (or (-> objects meta ::index-frames)
(let [lookup (d/getf objects)
xform (comp (remove #(= uuid/zero %))
(keep lookup)
(filter cfh/frame-shape?))]
(->> (keys objects)
(sequence xform))))
(remove #(or (and ^boolean skip-components?
^boolean (ctk/instance-head? %))
(and ^boolean skip-copies?
(and ^boolean (ctk/instance-head? %)
(not ^boolean (ctk/main-instance? %)))))))))
(defn get-frames-ids
"Retrieves all frame ids as vector"

View File

@@ -143,15 +143,6 @@
:gen/gen sg/text}
token-name-validation-regex])
(def token-ref-validation-regex
#"^\{[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*\}$")
(def schema:token-ref
"A token reference is a token name enclosed in {}."
[:re {:title "TokenRef"
:gen/gen sg/text}
token-ref-validation-regex])
(def schema:token-type
[::sm/one-of {:decode/json (fn [type]
(if (string? type)

View File

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

View File

@@ -50,7 +50,6 @@ services:
- 4400:4400
- 4401:4401
- 4402:4402
- 4403:4403
# Plugins
- 4200:4200

View File

@@ -68,7 +68,7 @@ RUN set -eux; \
--no-header-files \
--no-man-pages \
--strip-debug \
--add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported,jdk.jfr,jdk.jcmd \
--add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported \
--output /opt/jre;
FROM ubuntu:24.04 AS image

View File

@@ -30,9 +30,11 @@ x-uri: &penpot-public-uri
PENPOT_PUBLIC_URI: http://localhost:9001
x-body-size: &penpot-http-body-size
# Max body size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
# Deprecation warning: this variable is deprecated. Use PENPOT_HTTP_SERVER_MAX_BODY (defaults to 367001600)
# Max body size (30MiB); Used for plain requests, should never be
# greater than multi-part size
PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
# Max multipart body size (350MiB)
PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE: 367001600
## Penpot SECRET KEY. It serves as a master key from which other keys for subsystems

View File

@@ -30,8 +30,8 @@ update_flags /var/www/app/js/config.js
export PENPOT_BACKEND_URI=${PENPOT_BACKEND_URI:-http://penpot-backend:6060}
export PENPOT_EXPORTER_URI=${PENPOT_EXPORTER_URI:-http://penpot-exporter:6061}
export PENPOT_NITRATE_URI=${PENPOT_NITRATE_URI:-http://penpot-nitrate:3000}
export PENPOT_HTTP_SERVER_MAX_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_BODY_SIZE" \
export PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE=${PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE:-367001600} # Default to 350MiB
envsubst "\$PENPOT_BACKEND_URI,\$PENPOT_EXPORTER_URI,\$PENPOT_NITRATE_URI,\$PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE" \
< /tmp/nginx.conf.template > /etc/nginx/nginx.conf
PENPOT_DEFAULT_INTERNAL_RESOLVER="$(awk 'BEGIN{ORS=" "} $1=="nameserver" { sub(/%.*$/,"",$2); print ($2 ~ ":")? "["$2"]": $2}' /etc/resolv.conf)"

View File

@@ -76,7 +76,7 @@ http {
listen [::]:8080 default_server;
server_name _;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_BODY_SIZE;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE;
charset utf-8;
etag off;

View File

@@ -188,8 +188,8 @@ server {
server_name penpot.mycompany.com;
# This value should be in sync with the corresponding in the docker-compose.yml
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
client_max_body_size 367001600;
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 31457280
client_max_body_size 31457280;
# Logs: Configure your logs following the best practices inside your company
access_log /path/to/penpot.access.log;

View File

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

View File

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

View File

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

View File

@@ -1,814 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"render-wasm/v1",
"components/v2",
"fdata/shape-data-type"
]
},
"~:team-id": "~ueba8fa2e-4140-8084-8005-448635d7a724",
"~: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": "gaps",
"~:revn": 79,
"~:modified-at": "~m1771855365377",
"~:vern": 0,
"~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c863f",
"~: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": "~ueba8fa2e-4140-8084-8005-448635da32b4",
"~:created-at": "~m1771591980210",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640"
],
"~:pages-index": {
"~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640": {
"~:objects": {
"~u00000000-0000-0000-0000-000000000000": {
"~#shape": {
"~:y": 0,
"~:hide-fill-on-export": false,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:name": "Root Frame",
"~:width": 0.01,
"~:type": "~:frame",
"~:points": [
{
"~#point": {
"~:x": 0,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 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,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 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,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": [
"~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e",
"~ufbc43ead-a2ce-8058-8007-9a0daf843e09",
"~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8",
"~u5bebb998-d617-801b-8007-9a3fbd5cc804",
"~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40"
]
}
},
"~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8": {
"~#shape": {
"~:y": null,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:content": {
"~#penpot/path-data": "~bAQAAAAAAAAAAAAAAAAAAAAAAAAD/f5dEM2EsRAIAAAAAAAAAAAAAAAAAAAAAAAAAUhmnRABACkQCAAAAAAAAAAAAAAAAAAAAAAAAAP8/vET//01EAgAAAAAAAAAAAAAAAAAAAAAAAAD/f5dEM2EsRA=="
},
"~:name": "Path",
"~:width": null,
"~:type": "~:path",
"~:points": [
{
"~#point": {
"~:x": 1212.00003372852,
"~:y": 553.000012923003
}
},
{
"~#point": {
"~:x": 1506.00004755679,
"~:y": 553.000012923003
}
},
{
"~#point": {
"~:x": 1506.00004755679,
"~:y": 823.999993849517
}
},
{
"~#point": {
"~:x": 1212.00003372852,
"~:y": 823.999993849517
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~ufbc43ead-a2ce-8058-8007-9a0dbe2f49b8",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-alignment": "~:inner",
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-width": 10
}
],
"~:x": null,
"~:proportion": 1,
"~:shadow": [],
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 1212.00003372852,
"~:y": 553.000012923003,
"~:width": 294.000013828278,
"~:height": 270.999980926514,
"~:x1": 1212.00003372852,
"~:y1": 553.000012923003,
"~:x2": 1506.00004755679,
"~:y2": 823.999993849517
}
},
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": null,
"~:flip-y": null
}
},
"~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e": {
"~#shape": {
"~:y": 122.000001761754,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 463.999987447937,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 694.000014750112,
"~:y": 122.000001761754
}
},
{
"~#point": {
"~:x": 1158.00000219805,
"~:y": 122.000001761754
}
},
{
"~#point": {
"~:x": 1158.00000219805,
"~:y": 499.999980116278
}
},
{
"~#point": {
"~:x": 694.000014750112,
"~:y": 499.999980116278
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u36e8a3ad-2b63-8008-8007-9a0b2f24ca4e",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-alignment": "~:inner",
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-width": 100
},
{
"~:stroke-alignment": "~:outer",
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-width": 100
}
],
"~:x": 694.000014750113,
"~:proportion": 1,
"~:shadow": [],
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 694.000014750113,
"~:y": 122.000001761754,
"~:width": 463.999987447937,
"~:height": 377.999978354524,
"~:x1": 694.000014750113,
"~:y1": 122.000001761754,
"~:x2": 1158.00000219805,
"~:y2": 499.999980116278
}
},
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 377.999978354524,
"~:flip-y": null
}
},
"~ufbc43ead-a2ce-8058-8007-9a0daf843e09": {
"~#shape": {
"~:y": 262.999997589325,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:fixed",
"~:hide-in-viewer": false,
"~:name": "Ellipse",
"~:width": 266.000036716461,
"~:type": "~:circle",
"~:points": [
{
"~#point": {
"~:x": 1271.00000137752,
"~:y": 262.999997589325
}
},
{
"~#point": {
"~:x": 1537.00003809398,
"~:y": 262.999997589325
}
},
{
"~#point": {
"~:x": 1537.00003809398,
"~:y": 483.000033828949
}
},
{
"~#point": {
"~:x": 1271.00000137752,
"~:y": 483.000033828949
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~ufbc43ead-a2ce-8058-8007-9a0daf843e09",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-alignment": "~:inner",
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-width": 10
}
],
"~:x": 1271.00000137752,
"~:proportion": 1,
"~:shadow": [
{
"~:id": "~u9c6321b5-aeab-809f-8007-971f9e232191",
"~:style": "~:drop-shadow",
"~:color": {
"~:color": "#000000",
"~:opacity": 1
},
"~:offset-x": 4,
"~:offset-y": 4,
"~:blur": 0,
"~:spread": 0,
"~:hidden": true
}
],
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 1271.00000137752,
"~:y": 262.999997589325,
"~:width": 266.000036716461,
"~:height": 220.000036239624,
"~:x1": 1271.00000137752,
"~:y1": 262.999997589325,
"~:x2": 1537.00003809398,
"~:y2": 483.000033828949
}
},
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 220.000036239624,
"~:flip-y": null
}
},
"~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40": {
"~#shape": {
"~:y": -286.999972473494,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "1srkh8oc2vd",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "170uyffw5ph",
"~:font-size": "400",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "HELLO"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "psg8ayj675",
"~:font-size": "400",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "HELLO",
"~:width": 1116.00003953244,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 545.000013504691,
"~:y": -286.999972473494
}
},
{
"~#point": {
"~:x": 1661.00005303713,
"~:y": -286.999972473494
}
},
{
"~#point": {
"~:x": 1661.00005303713,
"~:y": 193.000017549648
}
},
{
"~#point": {
"~:x": 545.000013504691,
"~:y": 193.000017549648
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:id": "~u80e2fa5a-cd1c-8043-8007-9d8aaca49f40",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:position-data": [
{
"~:y": 211.980041503906,
"~:line-height": "1.2",
"~:font-style": "normal",
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:font-size": "400",
"~:font-weight": "400",
"~:text-direction": "ltr",
"~:width": 1115.22998046875,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:x": 545,
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:direction": "ltr",
"~:font-family": "sourcesanspro",
"~:height": 517.960021972656,
"~:text": "HELLO"
}
],
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-style": "~:solid",
"~:stroke-alignment": "~:inner",
"~:stroke-width": 5,
"~:stroke-color": "#000000",
"~:stroke-opacity": 1
}
],
"~:x": 545.000013504691,
"~:selrect": {
"~#rect": {
"~:x": 545.000013504691,
"~:y": -286.999972473494,
"~:width": 1116.00003953244,
"~:height": 479.999990023141,
"~:x1": 545.000013504691,
"~:y1": -286.999972473494,
"~:x2": 1661.00005303713,
"~:y2": 193.000017549648
}
},
"~:flip-x": null,
"~:height": 479.999990023141,
"~:flip-y": null
}
},
"~u5bebb998-d617-801b-8007-9a3fbd5cc804": {
"~#shape": {
"~:y": 543.00001095581,
"~:transform": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:rotation": 0,
"~:hide-in-viewer": false,
"~:name": "Rectangle",
"~:width": 463.999987447937,
"~:type": "~:rect",
"~:points": [
{
"~#point": {
"~:x": 693.999990768432,
"~:y": 543.00001095581
}
},
{
"~#point": {
"~:x": 1157.99997821637,
"~:y": 543.00001095581
}
},
{
"~#point": {
"~:x": 1157.99997821637,
"~:y": 920.999989310334
}
},
{
"~#point": {
"~:x": 693.999990768432,
"~:y": 920.999989310334
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1,
"~:b": 0,
"~:c": 0,
"~:d": 1,
"~:e": 0,
"~:f": 0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u5bebb998-d617-801b-8007-9a3fbd5cc804",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [
{
"~:stroke-alignment": "~:inner",
"~:stroke-style": "~:solid",
"~:stroke-color": "#000000",
"~:stroke-opacity": 1,
"~:stroke-width": 100
}
],
"~:x": 693.999990768432,
"~:proportion": 1,
"~:shadow": [],
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 693.999990768432,
"~:y": 543.00001095581,
"~:width": 463.999987447937,
"~:height": 377.999978354524,
"~:x1": 693.999990768432,
"~:y1": 543.00001095581,
"~:x2": 1157.99997821637,
"~:y2": 920.999989310334
}
},
"~:fills": [
{
"~:fill-color": "#ffffff",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 377.999978354524,
"~:flip-y": null
}
}
},
"~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c8640",
"~:name": "Page 1",
"~:background": "#000000"
}
},
"~:id": "~ueffcbebc-b8c8-802f-8007-9a0b2e2c863f",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because it is too large Load Diff

View File

@@ -1,21 +0,0 @@
{
"~:file-id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
"~:id": "~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4",
"~:created-at": "~m1771846681191",
"~:modified-at": "~m1771846681191",
"~:type": "fragment",
"~:backend": "db",
"~:data": {
"~:id": "~u95b23c15-79f9-81ba-8007-99d81b5290dd",
"~:name": "Page 1",
"~: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]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~: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\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\"]]]",
"~ucfb31a9c-83c2-806f-8007-9dbf43043be0": "[\"~#shape\",[\"^ \",\"~:y\",-218.99999605032087,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",5,\"~:p2\",5,\"~:p3\",5,\"~:p4\",5],\"~: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\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Container\",\"~:layout-align-items\",\"~:start\",\"~:width\",431.99994866329087,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-218.99999605032087]],[\"^J\",[\"^ \",\"~:x\",1040.9999299699698,\"~:y\",-177.00001533586985]],[\"^J\",[\"^ \",\"~:x\",608.9999813066789,\"~:y\",-177.00001533586985]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fill\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:layout-justify-content\",\"^C\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:strokes\",[],\"~:x\",608.9999813066788,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",608.9999813066788,\"~:y\",-218.99999605032087,\"^D\",431.99994866329087,\"~:height\",41.99998071445103,\"~:x1\",608.9999813066788,\"~:y1\",-218.99999605032087,\"~:x2\",1040.9999299699698,\"~:y2\",-177.00001533586985]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#ffc0cb\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",41.99998071445103,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\"]]]",
"~ucfb31a9c-83c2-806f-8007-9dbf43043be2": "[\"~#shape\",[\"^ \",\"~:y\",-178.00000568505413,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",false,\"~:name\",\"show / hide me\",\"~:width\",99.98206911702209,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-178.00000568505413]],[\"^:\",[\"^ \",\"~:x\",713.9820693746558,\"~:y\",-148.0000135081636]],[\"^:\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-148.0000135081636]]],\"~: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]],\"~:layout-item-v-sizing\",\"^=\",\"~:r3\",0,\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:r1\",0,\"~:hidden\",true,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be2\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",614.0000002576337,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",614.0000002576337,\"~:y\",-178.00000568505413,\"^6\",99.98206911702209,\"~:height\",29.999992176890544,\"~:x1\",614.0000002576337,\"~:y1\",-178.00000568505413,\"~:x2\",713.9820693746558,\"~:y2\",-148.0000135081636]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^P\",29.999992176890544,\"~:flip-y\",null]]",
"~ucfb31a9c-83c2-806f-8007-9dbf43043be3": "[\"~#shape\",[\"^ \",\"~:y\",-213.99999587313152,\"~:hide-fill-on-export\",false,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:hide-in-viewer\",true,\"~:name\",\"Full width\",\"~:width\",422.00001200500014,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-213.99999587313152]],[\"^<\",[\"^ \",\"~:x\",1036.0000059112394,\"~:y\",-182.00001303926604]],[\"^<\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-182.00001303926604]]],\"~:r2\",8,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^4\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"^@\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be3\",\"~:parent-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:frame-id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\",\"~:strokes\",[],\"~:x\",613.9999939062393,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",613.9999939062393,\"~:y\",-213.99999587313152,\"^8\",422.00001200500014,\"~:height\",31.999982833865488,\"~:x1\",613.9999939062393,\"~:y1\",-213.99999587313152,\"~:x2\",1036.0000059112394,\"~:y2\",-182.00001303926604]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#212426\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^O\",31.999982833865488,\"~:flip-y\",null,\"~:shapes\",[]]]",
"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf": "[\"~#shape\",[\"^ \",\"~:y\",-228.99999763039506,\"~: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\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Parent\",\"~:layout-align-items\",\"~:start\",\"~:width\",451.999905143128,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-228.99999763039506]],[\"^J\",[\"^ \",\"~:x\",1050.999920103893,\"~:y\",-167.0000160450801]],[\"^J\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-167.0000160450801]]],\"~:r2\",0,\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:fix\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",10,\"~:column-gap\",8],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u95b23c15-79f9-81ba-8007-99d81b5290dd\",\"~:layout-item-v-sizing\",\"~:auto\",\"~:r3\",0,\"~:layout-justify-content\",\"^C\",\"~:r1\",0,\"~:id\",\"~ucfb31a9c-83c2-806f-8007-9dbf43043bdf\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",599.0000149607649,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",599.0000149607649,\"~:y\",-228.99999763039506,\"^D\",451.999905143128,\"~:height\",61.99998158531497,\"~:x1\",599.0000149607649,\"~:y1\",-228.99999763039506,\"~:x2\",1050.999920103893,\"~:y2\",-167.0000160450801]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^1:\",61.99998158531497,\"~:flip-y\",null,\"~:shapes\",[\"~ucfb31a9c-83c2-806f-8007-9dbf43043be0\"]]]"
}
}
}
}

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": "test-bug-flex",
"~:revn": 114,
"~:modified-at": "~m1771846681183",
"~:vern": 0,
"~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
"~: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": "~m1771590560885",
"~:backend": "legacy-db",
"~:data": {
"~:pages": [
"~u95b23c15-79f9-81ba-8007-99d81b5290dd"
],
"~:pages-index": {
"~u95b23c15-79f9-81ba-8007-99d81b5290dd": {
"~#penpot/pointer": [
"~u3a4d7ec7-c391-8146-8007-9dd6c998fbc4",
{
"~:created-at": "~m1771846681187"
}
]
}
},
"~:id": "~u3a4d7ec7-c391-8146-8007-9a05c41da6b9",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -243,46 +243,6 @@ test("Renders a file with a closed path shape with multiple segments using strok
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders solid shadows after select all and zoom to selected", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-solid-shadows.json");
await workspace.goToWorkspace({
id: "93113137-fe66-80fb-8007-99ca9fd96841",
pageId: "93113137-fe66-80fb-8007-99ca9fd96842",
});
await workspace.waitForFirstRender();
await workspace.viewport.click();
await page.keyboard.press("ControlOrMeta+A");
const previousRenderCount = await workspace.getRenderCount();
await page.keyboard.press("f");
await workspace.waitForNextRender(previousRenderCount);
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders strokes with solid shadows", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-solid-strokes-shadows.json");
await workspace.goToWorkspace({
id: "93113137-fe66-80fb-8007-99cfd5cbf361",
pageId: "93113137-fe66-80fb-8007-99cfd5cbf362",
});
await workspace.waitForFirstRender();
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Renders a file with paths and svg attrs", async ({ page }) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
@@ -432,27 +392,3 @@ test("Keeps component visible when focusing after creating it", async ({
await workspace.hideUI();
await expect(workspace.canvas).toHaveScreenshot();
});
test("Check inner stroke artifacts", async ({
page,
}) => {
const workspace = new WasmWorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("render-wasm/get-file-inner-strokes-artifacts.json");
await workspace.goToWorkspace({
id: "effcbebc-b8c8-802f-8007-9a0b2e2c863f",
pageId: "effcbebc-b8c8-802f-8007-9a0b2e2c8640",
});
await workspace.waitForFirstRenderWithoutUI();
const previousRenderCount = await workspace.getRenderCount();
await page.keyboard.press("ControlOrMeta++");
await workspace.waitForNextRender(previousRenderCount);
// Stricter comparison: artifacts are very subtle
await expect(workspace.canvas).toHaveScreenshot({
maxDiffPixelRatio: 0,
threshold: 0.1,
});
});

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 114 KiB

View File

@@ -97,7 +97,6 @@ test("Update an already created text shape by prepending text", async ({
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(0);
await page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
await page.keyboard.type("Dolor sit amet ");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Dolor sit amet Lorem ipsum");

View File

@@ -1,21 +1,22 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import {
setupTokensFileRender,
setupTypographyTokensFileRender,
setupEmptyTokensFile,
setupTokensFile,
setupTypographyTokensFile,
unfoldTokenTree,
} from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
test.describe("Tokens: Apply token", () => {
test("User applies color token to a shape", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
@@ -43,7 +44,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
@@ -104,7 +105,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
@@ -168,7 +169,7 @@ test.describe("Tokens: Apply token", () => {
test("User applies typography token to a text shape", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTypographyTokensFileRender(page);
await setupTypographyTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
@@ -202,7 +203,7 @@ test.describe("Tokens: Apply token", () => {
tokensSidebar,
workspacePage,
tokenContextMenuForToken,
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -488,7 +489,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
@@ -539,7 +540,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
@@ -593,7 +594,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
@@ -647,7 +648,7 @@ test.describe("Tokens: Apply token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
@@ -700,7 +701,7 @@ test.describe("Tokens: Apply token", () => {
});
test("User applies stroke width token to a shape", async ({ page }) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
// Set up
@@ -760,7 +761,7 @@ test.describe("Tokens: Apply token", () => {
});
test("User applies margin token to a shape", async ({ page }) => {
const workspace = new WasmWorkspacePage(page, {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
// Set up
@@ -852,7 +853,7 @@ test.describe("Tokens: Detach token", () => {
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();

View File

@@ -1,16 +1,16 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import {
setupEmptyTokensFileRender,
setupTokensFileRender,
setupTypographyTokensFileRender,
setupEmptyTokensFile,
setupTokensFile,
setupTypographyTokensFile,
testTokenCreationFlow,
unfoldTokenTree,
} from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
@@ -158,7 +158,7 @@ test.describe("Tokens - creation", () => {
const selfReferenceError = "Token has self reference";
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
@@ -320,7 +320,7 @@ test.describe("Tokens - creation", () => {
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -465,7 +465,7 @@ test.describe("Tokens - creation", () => {
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -601,7 +601,7 @@ test.describe("Tokens - creation", () => {
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -717,7 +717,7 @@ test.describe("Tokens - creation", () => {
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -831,7 +831,7 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1012,7 +1012,7 @@ test.describe("Tokens - creation", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFileRender(page);
await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1047,7 +1047,7 @@ test.describe("Tokens - creation", () => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page, { flags: ["enable-token-shadow"] });
await setupEmptyTokensFile(page, { flags: ["enable-token-shadow"] });
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1232,7 +1232,7 @@ test.describe("Tokens - creation", () => {
test("User creates typography token", async ({ page }) => {
const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -1479,7 +1479,7 @@ test.describe("Tokens - creation", () => {
test("User adds typography token with reference", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFileRender(page);
await setupTypographyTokensFile(page);
const newTokenTitle = "NewReference";
@@ -1529,7 +1529,7 @@ test.describe("Tokens - creation", () => {
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
@@ -1562,7 +1562,7 @@ test.describe("Tokens - creation", () => {
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1589,7 +1589,7 @@ test.describe("Tokens - creation", () => {
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1613,7 +1613,7 @@ test.describe("Tokens - creation", () => {
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
await tokensSidebar.getByRole("button", { name: "Add Token: Color" }).click();
@@ -1642,7 +1642,7 @@ test("User creates grouped color token", async ({ page }) => {
});
test("User cant create regular token with value missing", async ({ page }) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFileRender(page);
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1669,7 +1669,7 @@ test("User cant create regular token with value missing", async ({ page }) => {
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1695,7 +1695,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFileRender(page);
await setupTypographyTokensFile(page);
await tokensSidebar
.getByRole("button")
@@ -1791,7 +1791,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1827,7 +1827,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
@@ -1882,7 +1882,7 @@ test.describe("Tokens tab - edition", () => {
test.describe("Tokens tab - delete", () => {
test("User delete color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1902,7 +1902,7 @@ test.describe("Tokens tab - delete", () => {
});
test("User removes node and all child tokens", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page);
const { tokensSidebar } = await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();

View File

@@ -1,10 +1,10 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { setupEmptyTokensFileRender } from "./helpers";
import { setupEmptyTokensFile } from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
@@ -12,7 +12,7 @@ test.describe("Tokens tab - common tests", () => {
test("Clicking tokens tab button opens tokens sidebar tab", async ({
page,
}) => {
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });

View File

@@ -1,6 +1,5 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
const setupEmptyTokensFile = async (page, options = {}) => {
const { flags = [] } = options;
@@ -41,45 +40,6 @@ const setupEmptyTokensFile = async (page, options = {}) => {
};
};
const setupEmptyTokensFileRender = async (page, options = {}) => {
const { flags = [] } = options;
const workspacePage = new WasmWorkspacePage(page);
if (flags.length > 0) {
await workspacePage.mockConfigFlags(flags);
}
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"get-team?id=*",
"workspace/get-team-tokens.json",
);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "c7ce0794-0992-8105-8004-38f280443849",
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
});
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
return {
workspacePage,
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
tokenSetItems: workspacePage.tokenSetItems,
tokensSidebar: workspacePage.tokensSidebar,
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
};
};
const setupTokensFile = async (page, options = {}) => {
const {
file = "workspace/get-file-tokens.json",
@@ -125,51 +85,6 @@ const setupTokensFile = async (page, options = {}) => {
};
};
const setupTokensFileRender = async (page, options = {}) => {
const {
file = "workspace/get-file-tokens.json",
fileFragment = "workspace/get-file-fragment-tokens.json",
flags = ["enable-feature-token-input"],
} = options;
const workspacePage = new WasmWorkspacePage(page);
if (flags.length > 0) {
await workspacePage.mockConfigFlags(flags);
}
await workspacePage.setupEmptyFile();
await workspacePage.mockRPC(
"get-team?id=*",
"workspace/get-team-tokens.json",
);
await workspacePage.mockRPC(/get\-file\?/, file);
await workspacePage.mockRPC(/get\-file\-fragment\?/, fileFragment);
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-create-rect.json",
);
await workspacePage.goToWorkspace({
fileId: "c7ce0794-0992-8105-8004-38f280443849",
pageId: "66697432-c33d-8055-8006-2c62cc084cad",
});
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
return {
workspacePage,
tokensUpdateCreateModal: workspacePage.tokensUpdateCreateModal,
tokenThemeUpdateCreateModal: workspacePage.tokenThemeUpdateCreateModal,
tokenThemesSetsSidebar: workspacePage.tokenThemesSetsSidebar,
tokenSetItems: workspacePage.tokenSetItems,
tokenSetGroupItems: workspacePage.tokenSetGroupItems,
tokensSidebar: workspacePage.tokensSidebar,
tokenContextMenuForToken: workspacePage.tokenContextMenuForToken,
tokenContextMenuForSet: workspacePage.tokenContextMenuForSet,
};
};
const setupTypographyTokensFile = async (page, options = {}) => {
return setupTokensFile(page, {
file: "workspace/get-file-typography-tokens.json",
@@ -178,14 +93,6 @@ const setupTypographyTokensFile = async (page, options = {}) => {
});
};
const setupTypographyTokensFileRender = async (page, options = {}) => {
return setupTokensFileRender(page, {
file: "workspace/get-file-typography-tokens.json",
fileFragment: "workspace/get-file-fragment-typography-tokens.json",
...options,
});
};
const testTokenCreationFlow = async (
page,
{
@@ -207,7 +114,7 @@ const testTokenCreationFlow = async (
const missingReferenceError = "Missing token references";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
// Open modal
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -352,11 +259,8 @@ const unfoldTokenTree = async (tokensTabPanel, type, tokenName) => {
export {
setupEmptyTokensFile,
setupEmptyTokensFileRender,
setupTokensFile,
setupTokensFileRender,
setupTypographyTokensFile,
setupTypographyTokensFileRender,
testTokenCreationFlow,
unfoldTokenTree,
};

View File

@@ -1,23 +1,21 @@
import { test, expect } from "@playwright/test";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import {
setupTokensFileRender,
setupTypographyTokensFileRender,
setupEmptyTokensFile,
setupTokensFile,
setupTypographyTokensFile,
} from "./helpers";
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, [
"enable-feature-design-tokens-v1",
]);
await WasmWorkspacePage.mockRPC(page, "get-teams", "get-teams-tokens.json");
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
const createToken = async (page, type, name, textFieldName, value) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
@@ -44,7 +42,7 @@ const createToken = async (page, type, name, textFieldName, value) => {
const renameToken = async (page, oldName, newName) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
await setupTokensFile(page, { flags: ["enable-token-shadow"] });
const baseToken = tokensSidebar.getByRole("button", {
name: oldName,
@@ -66,7 +64,7 @@ const renameToken = async (page, oldName, newName) => {
const createCompositeDerivedToken = async (page, type, name, reference) => {
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
const { tokensUpdateCreateModal } = await setupTokensFileRender(page, {
const { tokensUpdateCreateModal } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
@@ -100,7 +98,7 @@ test.describe("Remapping Tokens", () => {
test("User renames box shadow token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
@@ -146,7 +144,7 @@ test.describe("Remapping Tokens", () => {
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTokensFileRender(page, { flags: ["enable-token-shadow"] });
} = await setupTokensFile(page, { flags: ["enable-token-shadow"] });
// Create base shadow token
await createToken(page, "Shadow", "primary-shadow", "Color", "#000000");
@@ -251,7 +249,7 @@ test.describe("Remapping Tokens", () => {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTypographyTokensFileRender(page);
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -295,7 +293,7 @@ test.describe("Remapping Tokens", () => {
tokensSidebar,
tokenContextMenuForToken,
workspacePage,
} = await setupTypographyTokensFileRender(page);
} = await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
@@ -403,7 +401,7 @@ test.describe("Remapping Tokens", () => {
test("User renames border radius token with alias references", async ({
page,
}) => {
const { tokensSidebar } = await setupTokensFileRender(page);
const { tokensSidebar } = await setupTokensFile(page);
// Create base border radius token
await createToken(page, "Border Radius", "base-radius", "Value", "4");
@@ -445,7 +443,7 @@ test.describe("Remapping Tokens", () => {
tokensUpdateCreateModal,
tokensSidebar,
tokenContextMenuForToken,
} = await setupTokensFileRender(page);
} = await setupTokensFile(page);
// Create base border radius token
await createToken(page, "Border Radius", "radius-sm", "Value", "4");
@@ -514,7 +512,7 @@ test.describe("Remapping Tokens", () => {
test.describe("Cancel remap", () => {
test("Only rename - breaks reference", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});
@@ -553,7 +551,7 @@ test.describe("Remapping Tokens", () => {
});
test("Cancel process - no changes applied", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page, {
const { tokensSidebar } = await setupTokensFile(page, {
flags: ["enable-token-shadow"],
});

View File

@@ -1,10 +1,10 @@
import { test, expect } from "@playwright/test";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { setupEmptyTokensFileRender, setupTokensFileRender } from "./helpers";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { setupEmptyTokensFile, setupTokensFile } from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
@@ -42,7 +42,7 @@ test.describe("Tokens: Sets Tab", () => {
page,
}) => {
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
const tokensTabButton = tokenThemesSetsSidebar
.getByRole("button", { name: "Add set" })
@@ -139,7 +139,7 @@ test.describe("Tokens: Sets Tab", () => {
page,
}) => {
const { tokenThemesSetsSidebar, tokenContextMenuForSet } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
const tokensTabButton = tokenThemesSetsSidebar
.getByRole("button", { name: "Add set" })
@@ -176,7 +176,7 @@ test.describe("Tokens: Sets Tab", () => {
test("Fold/Unfold set", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenSetGroupItems } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokenThemesSetsSidebar).toBeVisible();
@@ -202,7 +202,7 @@ test.describe("Tokens: Sets Tab", () => {
test("Change current theme", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenSetItems } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokenSetItems.nth(1)).toHaveAttribute("aria-checked", "true");
await expect(tokenSetItems.nth(2)).toHaveAttribute("aria-checked", "false");
@@ -219,7 +219,7 @@ test.describe("Tokens: Sets Tab", () => {
test("Display active set and verify if is enabled", async ({ page }) => {
const { tokenThemesSetsSidebar, tokensSidebar, tokenSetItems } =
await setupTokensFileRender(page);
await setupTokensFile(page);
// Create set
await tokenThemesSetsSidebar

View File

@@ -1,7 +1,7 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { setupEmptyTokensFileRender, setupTokensFileRender } from "./helpers";
import { setupEmptyTokensFile, setupTokensFile } from "./helpers";
// THEMES HELPERS
@@ -23,17 +23,14 @@ const checkInputFieldWithoutError = async (inputLocator) => {
};
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WasmWorkspacePage.mockConfigFlags(page, [
"enable-feature-design-tokens-v1",
]);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
test.describe("Tokens Themes", () => {
test("User edits theme and activates it in the sidebar", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenThemeUpdateCreateModal } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokenThemesSetsSidebar).toBeVisible();
@@ -120,7 +117,7 @@ test.describe("Tokens Themes", () => {
test.describe("Tokens: Themes modal", () => {
test("Delete theme", async ({ page }) => {
const { tokenThemeUpdateCreateModal, workspacePage } =
await setupTokensFileRender(page);
await setupTokensFile(page);
workspacePage.openTokenThemesModal();
@@ -140,7 +137,7 @@ test.describe("Tokens: Themes modal", () => {
test("Add new theme in empty file", async ({ page }) => {
const { tokenThemesSetsSidebar, tokenThemeUpdateCreateModal } =
await setupEmptyTokensFileRender(page);
await setupEmptyTokensFile(page);
await tokenThemesSetsSidebar
.getByRole("button", { name: "Create one." })
@@ -173,7 +170,7 @@ test.describe("Tokens: Themes modal", () => {
test("Add new theme", async ({ page }) => {
const { tokenThemeUpdateCreateModal, workspacePage } =
await setupTokensFileRender(page);
await setupTokensFile(page);
workspacePage.openTokenThemesModal();
@@ -213,7 +210,7 @@ test.describe("Tokens: Themes modal", () => {
test("Edit theme", async ({ page }) => {
const { tokenThemeUpdateCreateModal, workspacePage } =
await setupTokensFileRender(page);
await setupTokensFile(page);
workspacePage.openTokenThemesModal();

View File

@@ -1,16 +1,16 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../../pages/WasmWorkspacePage";
import { WorkspacePage } from "../../pages/WorkspacePage";
import { BaseWebSocketPage } from "../../pages/BaseWebSocketPage";
import { setupTokensFileRender, unfoldTokenTree } from "./helpers";
import { setupTokensFile, unfoldTokenTree } from "./helpers";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
test.describe("Tokens - node tree", () => {
test("User fold/unfold color tokens", async ({ page }) => {
const { tokensSidebar } = await setupTokensFileRender(page);
const { tokensSidebar } = await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();

View File

@@ -55,31 +55,3 @@ test("BUG 13382 - Fix problem with flex layout", async ({ page }) => {
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("340");
});
test("BUG 13468 - Fix problem with flex propagation", async ({ page }) => {
const workspacePage = new WasmWorkspacePage(page);
await workspacePage.setupEmptyFile();
await workspacePage.mockGetFile("workspace/get-file-13468.json");
await workspacePage.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"workspace/get-file-13468-fragment.json",
);
await workspacePage.mockRPC("update-file?id=*", "workspace/update-file-empty.json");
await workspacePage.goToWorkspace({
fileId: "3a4d7ec7-c391-8146-8007-9a05c41da6b9",
pageId: "95b23c15-79f9-81ba-8007-99d81b5290dd",
});
0
await workspacePage.clickToggableLayer("Parent");
await workspacePage.clickToggableLayer("Container");
await workspacePage.sidebar.getByRole('button', { name: 'Show' }).click();
await workspacePage.clickLeafLayer("Container");
await expect(workspacePage.rightSidebar.getByTitle("Height").getByRole("textbox")).toHaveValue("76");
});

View File

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

View File

@@ -18,7 +18,7 @@
<meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& version_tag}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& ts}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}}

View File

@@ -288,7 +288,6 @@ export async function compileTranslations() {
"es",
"fa",
"fr",
"fr_CA",
"he",
"sr",
"nb_NO",

View File

@@ -4,9 +4,4 @@ TARGET=${1:-app};
set -ex
rm -rf node_modules;
corepack enable;
corepack install;
pnpm install;
pnpm run watch:$TARGET
exec pnpm run watch:$TARGET

View File

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

View File

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

View File

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

View File

@@ -1,28 +0,0 @@
(ns app.main.data.nitrate
(:require
[app.main.data.modal :as modal]
[app.main.repo :as rp]
[app.main.router :as rt]
[app.main.store :as st]
[app.util.dom :as dom]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn show-nitrate-popup
[popup-type]
(ptk/reify ::show-nitrate-popup
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! ::get-nitrate-connectivity {})
(rx/map (fn [connectivity]
(modal/show popup-type (or connectivity {}))))))))
(defn go-to-nitrate-cc
[]
(st/emit! (dom/open-new-window "/control-center/")))
(defn go-to-nitrate-billing
[]
(st/emit! (rt/nav-raw :href "/control-center/licenses/billing")))

View File

@@ -14,7 +14,6 @@
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.store :as st]
[app.plugins.flags :as pflag]
[app.plugins.register :as preg]
[app.util.globals :as ug]
[app.util.http :as http]
@@ -45,6 +44,20 @@
(update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil conj #{}) id))))
(defn reset-plugin-flags
[id]
(ptk/reify ::reset-plugin-flags
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :plugin-flags] assoc id {}))))
(defn set-plugin-flag
[id key value]
(ptk/reify ::set-plugin-flag
ptk/UpdateEvent
(update [_ state]
(update-in state [:workspace-local :plugin-flags id] assoc key value))))
(defn remove-current-plugin
[id]
(ptk/reify ::remove-current-plugin
@@ -52,26 +65,11 @@
(update [_ state]
(update-in state [:workspace-local :open-plugins] (fnil disj #{}) id))))
(defn start-plugin!
[{:keys [plugin-id name version description host code permissions allow-background]} ^js extensions]
(.ɵloadPlugin
^js ug/global
#js {:pluginId plugin-id
:name name
:version version
:description description
:host host
:code code
:allowBackground (boolean allow-background)
:permissions (apply array permissions)}
nil
extensions))
(defn- load-plugin!
[{:keys [plugin-id name description host code icon permissions] :as params}]
[{:keys [plugin-id name description host code icon permissions]}]
(try
(st/emit! (pflag/clear plugin-id)
(save-current-plugin plugin-id))
(st/emit! (save-current-plugin plugin-id)
(reset-plugin-flags plugin-id))
(.ɵloadPlugin
^js ug/global

View File

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

View File

@@ -69,10 +69,6 @@
(and (number-with-unit-symbol? v)
(= (.-unit v) "rem")))
(defn percent-number-with-unit? [v]
(and (number-with-unit-symbol? v)
(= (.-unit v) "%")))
(defn rem->px [^js v]
(* (.-value v) 16))
@@ -83,7 +79,7 @@
Structured tokens are non-primitive token types like `typography` or `box-shadow`."
[^js token-symbol]
(if (instance? js/Array (.-value token-symbol))
(mapv tokenscript-symbols->penpot-unit (.-value token-symbol))
(mapv structured-token->penpot-map (.-value token-symbol))
(let [entries (es6-iterator-seq (.entries (.-value token-symbol)))]
(into {} (map (fn [[k v :as V]]
[(keyword k) (tokenscript-symbols->penpot-unit v)])
@@ -91,12 +87,10 @@
(defn tokenscript-symbols->penpot-unit [^js v]
(cond
(nil? v) nil
(structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (structured-token->penpot-map v)
(list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v))
(color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v)
(percent-number-with-unit? v) (/ (.-value v) 100)
:else (.-value v)))
;; Processors ------------------------------------------------------------------

View File

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

View File

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

View File

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

View File

@@ -221,24 +221,6 @@
(pcb/delete-color id))]
(rx/of (dch/commit-changes changes))))))
(defn duplicate-color
[file-id color-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (uuid? color-id) "expected valid uuid for `color-id`")
(ptk/reify ::duplicate-color
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
color (ctl/get-color data color-id)
new-color (-> color
(assoc :id (uuid/next))
(d/without-nils)
(ctc/check-library-color))
changes (-> (pcb/empty-changes it)
(pcb/add-color new-color))]
(rx/of (dch/commit-changes changes))))))
;; FIXME: this should be deleted
(defn add-media
[media]
@@ -368,23 +350,6 @@
(pcb/delete-typography id))]
(rx/of (dch/commit-changes changes))))))
(defn duplicate-typography
[file-id typography-id]
(assert (uuid? file-id) "expected valid uuid for `file-id`")
(assert (uuid? typography-id) "expected valid uuid for `typography-id`")
(ptk/reify ::duplicate-typography
ptk/WatchEvent
(watch [it state _]
(let [data (dsh/lookup-file-data state)
typography (get-in data [:typographies typography-id])
new-typography (-> typography
(assoc :id (uuid/next))
(ctt/check-typography))
changes (-> (pcb/empty-changes it)
(pcb/add-typography new-typography))]
(rx/of (dch/commit-changes changes))))))
(defn- add-component2
"This is the second step of the component creation."
([selected]

View File

@@ -1,87 +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.main.store :as st]
[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 finalize-workspace?
[event]
(= (ptk/type event) :app.main.data.workspace/finalize-workspace))
(defn init-mcp!
[stream]
(->> (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))
:on
(fn [event cb]
(when-let [event
(case event
"disconnect" ::disconnect
"connect" ::connect
nil)]
(let [stopper (rx/filter finalize-workspace? stream)]
(->> stream
(rx/filter (ptk/type? event))
(rx/take-until stopper)
(rx/subs! #(cb))))))}}))))))
(defn disconnect-mcp
[]
(st/emit! (ptk/data-event ::disconnect)))
(defn connect-mcp
[]
(st/emit! (ptk/data-event ::connect)))
(defn init-mcp-connexion
[]
(ptk/reify ::init-mcp-connexion
ptk/EffectEvent
(effect [_ state stream]
(when (and (contains? cf/flags :mcp)
(-> state :profile :props :mcp-status))
(init-mcp! stream)))))

View File

@@ -8,11 +8,10 @@
(:require
[app.common.json :as json]
[app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.notifications :as ntf]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.store :as st]
[app.util.i18n :refer [tr]]
@@ -75,18 +74,15 @@
(when unknown-tokens
(st/emit! (show-unknown-types-warning unknown-tokens)))
(try
(let [tokens-tree (ctob/get-all-tokens-map tokens-lib)
resolved-tokens (if (contains? cf/flags :tokenscript)
(rx/of (ts/resolve-tokens tokens-tree))
(sd/resolve-tokens-with-verbose-errors tokens-tree))]
(->> resolved-tokens
(rx/map (fn [_]
tokens-lib))
(rx/catch (fn [sd-error]
(let [reference-errors (extract-reference-errors sd-error)]
(if reference-errors
(rx/of tokens-lib)
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error))))))))
(->> (ctob/get-all-tokens-map tokens-lib)
(sd/resolve-tokens-with-verbose-errors)
(rx/map (fn [_]
tokens-lib))
(rx/catch (fn [sd-error]
(let [reference-errors (extract-reference-errors sd-error)]
(if reference-errors
(rx/of tokens-lib)
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error sd-error sd-error)))))))
(catch js/Error e
(throw (wte/error-ex-info :error.import/style-dictionary-unknown-error "" e)))))

View File

@@ -6,16 +6,13 @@
(ns app.main.data.workspace.tokens.propagation
(:require
[app.common.data :as d]
[app.common.files.helpers :as cfh]
[app.common.logging :as l]
[app.common.time :as ct]
[app.common.types.token :as ctt]
[app.common.types.tokens-lib :as ctob]
[app.config :as cf]
[app.main.data.helpers :as dsh]
[app.main.data.style-dictionary :as sd]
[app.main.data.tokenscript :as ts]
[app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.thumbnails :as dwt]
[app.main.data.workspace.tokens.application :as dwta]
@@ -213,13 +210,10 @@
(ptk/reify ::propagate-workspace-tokens
ptk/WatchEvent
(watch [_ state _]
(when-let [tokens-tree (-> (dsh/lookup-file-data state)
(get :tokens-lib)
(ctob/get-tokens-in-active-sets))]
(->> (if (contains? cf/flags :tokenscript)
(rx/of (-> (ts/resolve-tokens tokens-tree)
(d/update-vals #(update % :resolved-value ts/tokenscript-symbols->penpot-unit))))
(sd/resolve-tokens tokens-tree))
(when-let [tokens-lib (-> (dsh/lookup-file-data state)
(get :tokens-lib))]
(->> (ctob/get-tokens-in-active-sets tokens-lib)
(sd/resolve-tokens)
(rx/mapcat (fn [sd-tokens]
(let [undo-id (js/Symbol)]
(rx/concat

View File

@@ -1173,8 +1173,7 @@
(when add-component-to-variant?
(rx/of (ev/event {::ev/name "add-component-to-variant"})))
(when add-new-variant?
(rx/of (ev/event {::ev/name "add-new-variant"
::ev/origin "workspace:move-shapes-to-frame"}))))))))
(rx/of (ev/event {::ev/name "add-new-variant" ::ev/origin "workspace:move-shapes-to-frame"}))))))))
(defn- get-displacement
"Retrieve the correct displacement delta point for the

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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