Compare commits

..

33 Commits

Author SHA1 Message Date
Aitor Moreno
b2e689a645 ♻️ Refactor TextCursor and TextPositionWithAffinity 2026-02-24 15:56:54 +01:00
Elena Torró
32d4026641 Merge pull request #8338 from penpot/azazeln28-11826-compute-selection-rects-from-pointer-events
🎉 Add compute selection rects from pointer events
2026-02-24 12:53:08 +01:00
Aitor Moreno
4477b2b4a0 🎉 Compute selection rects from pointer events 2026-02-24 11:09:45 +01:00
Andrey Antukh
9e51fa198a Merge remote-tracking branch 'origin/staging' into staging-render 2026-02-24 00:09:41 +01:00
Andrey Antukh
d176da8012 Add jfr and jcmd to the backend docker image (#8446) 2026-02-24 00:08:14 +01:00
Andrey Antukh
20862c2da3 🐛 Fix incorrect plugin icon resolution 2026-02-24 00:07:30 +01:00
Andrey Antukh
1b8afccba2 Remove usage of multipart body size config on backend 2026-02-23 14:44:44 +01:00
Yamila Moreno
dd856ecf50 ♻️ Deprecate PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE envvar 2026-02-23 13:48:01 +01:00
Elena Torró
f4e79af3cd Merge pull request #8438 from penpot/alotor-fix-flex-layout-issue
🐛 Fix problem with flex layout propagation
2026-02-23 13:09:04 +01:00
alonso.torres
3e758826fe 🐛 Fix problem with flex layout propagation 2026-02-23 12:49:27 +01:00
Aitor Moreno
2cf66c948d Merge pull request #8427 from penpot/superalex-fix-blur-0-artifacts-2
🐛 Fix blur 0 artifacts
2026-02-23 12:25:54 +01:00
Andrey Antukh
145198c148 📎 Use proper version tag on frontend index template 2026-02-23 12:17:58 +01:00
alonso.torres
eddfc4c4b2 🐛 Fix problem with createText in plugins 2026-02-23 09:35:30 +01:00
alonso.torres
e6e34af391 🐛 Show outline on hidden paths 2026-02-23 09:34:50 +01:00
Alejandro Alonso
4ee908fc89 Revert "🐛 Fix stroke artifacts"
This reverts commit bdcf448f3f.
2026-02-23 07:23:41 +01:00
Alejandro Alonso
bdcf448f3f 🐛 Fix stroke artifacts 2026-02-23 07:23:12 +01:00
Aitor Moreno
c58054d19c Merge pull request #8408 from penpot/ladybenko-always-mock-config-playwright
🔧 Always mock config.js in Playwright
2026-02-20 14:26:45 +01:00
Alejandro Alonso
a7ab506c5c 🐛 Fix blur 0 artifacts 2026-02-20 13:37:27 +01:00
Alejandro Alonso
c7f644ab2a Merge pull request #8420 from penpot/elenatorro-13426-improve-pan-and-zoom-for-blur
🔧 Improve performance on shapes with blur
2026-02-20 12:49:24 +01:00
Andrés Moya
3d41dc276e 🐛 Fix resolve tokens with tokenscript when type is font family 2026-02-20 12:41:17 +01:00
Elena Torró
cb5cacbcee Merge pull request #8413 from penpot/alotor-fix-error-emtpy-text
🐛 Fix problem with empty text
2026-02-19 17:09:03 +01:00
Elena Torro
337cfc2d3e 🔧 Improve performance on shapes with blur 2026-02-19 16:50:42 +01:00
Belén Albeza
c2ee31e791 Fix some flaky text editor v2 tests 2026-02-19 15:46:16 +01:00
alonso.torres
360937f613 🐛 Fix problem with empty text 2026-02-19 12:00:05 +01:00
Belén Albeza
f6d0414449 🔧 Use config flag for variants tests 2026-02-19 09:56:28 +01:00
Belén Albeza
4d05827fa9 🔧 Use disable-onboarding config flag instead of mocking get-profile in playwright 2026-02-19 09:56:27 +01:00
Belén Albeza
48fb9fa6ea Fix broken playwright tests 2026-02-19 09:56:27 +01:00
alonso.torres
cee974a906 🐛 Fix problem with tokens in plugins 2026-02-18 17:20:46 +01:00
Belén Albeza
c74cf3fa37 🔧 Make config.js automatically mocked in playwright tests 2026-02-18 16:54:03 +01:00
Aitor Moreno
7c1ddd3d7d Merge pull request #8382 from penpot/alotor-fix-components-propagation
🐛 Fix problem with text change component propagation and undo
2026-02-18 10:37:06 +01:00
Alejandro Alonso
4965f6d859 Merge pull request #8394 from penpot/elenatorro-fix-watch
🔧 Fix watch script
2026-02-18 10:11:44 +01:00
Elena Torro
a3cd90da7f 🔧 Fix watch script 2026-02-18 09:57:25 +01:00
alonso.torres
30106f8524 🐛 Fix problem with text change component propagation and undo 2026-02-17 11:54:33 +01:00
164 changed files with 6403 additions and 3471 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,26 +1,9 @@
# CHANGELOG
## 2.15.0 (Unreleased)
## 2.14.0 (Unreleased)
### :boom: Breaking changes & Deprecations
### :rocket: Epics and highlights
### :heart: Community contributions (Thank you!)
### :sparkles: New features & Enhancements
- Add MCP server integration [Taiga #13112](https://tree.taiga.io/project/penpot/us/13112), [Taiga #13114](https://tree.taiga.io/project/penpot/us/13114)
- Add woff2 support on user uploaded fonts (by @Nivl) [Github #8248](https://github.com/penpot/penpot/pull/8248)
- Option to download custom fonts (by @dfelinto) [Github #8320](https://github.com/penpot/penpot/issues/8320)
- Add copy as image to clipboard option to workspace context menu (by @dfelinto) [Github #8313](https://github.com/penpot/penpot/pull/8313)
### :bug: Bugs fixed
- Fix Alt/Option to draw shapes from center point (by @offreal) [Github #8361](https://github.com/penpot/penpot/pull/8361)
## 2.14.0 (Unreleased)
- Deprecate `PENPOT_HTTP_SERVER_MAX_MULTIPART_BODY_SIZE` in favour of `PENPOT_HTTP_SERVER_MAX_BODY_SIZE`.
### :sparkles: New features & Enhancements
@@ -48,7 +31,6 @@
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
- Fix viewer can update library [Taiga #13186](https://tree.taiga.io/project/penpot/issue/13186)
- Fix remove fill affects different element than selected [Taiga #13128](https://tree.taiga.io/project/penpot/issue/13128)
- Fix unable to finish the create account form using keyboard [Taiga #11333](https://tree.taiga.io/project/penpot/issue/11333)
## 2.13.3

View File

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

View File

@@ -98,7 +98,6 @@
[: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]

View File

@@ -42,8 +42,8 @@
(def default-params
{::port 6060
::host "0.0.0.0"
::max-body-size 31457280 ; default 30 MiB
::max-multipart-body-size 367001600}) ; default 350 MiB
::max-body-size 367001600 ; default 350 MiB
})
(defmethod ig/expand-key ::server
[k v]
@@ -56,7 +56,6 @@
[::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]])
@@ -79,7 +78,7 @@
{:http/port port
:http/host host
:http/max-body-size (::max-body-size cfg)
:http/max-multipart-body-size (::max-multipart-body-size cfg)
:http/max-multipart-body-size (::max-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

@@ -226,11 +226,10 @@
::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/max-multipart-body-size (cf/get :http-server-max-multipart-body-size)
::http/router (ig/ref ::http/router)
::mtx/metrics (ig/ref ::mtx/metrics)}
::ldap/provider

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
@@ -138,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

@@ -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

@@ -605,31 +605,31 @@
add-undo-change-shape
(fn [change-set id]
(let [shape (get objects id)]
(conj
change-set
{:type :add-obj
:id id
:page-id page-id
:parent-id (:parent-id shape)
:frame-id (:frame-id shape)
:index (cfh/get-position-on-parent objects id)
:obj (cond-> shape
(contains? shape :shapes)
(assoc :shapes []))})))
(cond-> change-set
(some? shape)
(conj {:type :add-obj
:id id
:page-id page-id
:parent-id (:parent-id shape)
:frame-id (:frame-id shape)
:index (cfh/get-position-on-parent objects id)
:obj (cond-> shape
(contains? shape :shapes)
(assoc :shapes []))}))))
add-undo-change-parent
(fn [change-set id]
(let [shape (get objects id)
prev-sibling (cfh/get-prev-sibling objects (:id shape))]
(conj
change-set
{:type :mov-objects
:page-id page-id
:parent-id (:parent-id shape)
:shapes [id]
:after-shape prev-sibling
:index 0
:ignore-touched true})))]
(cond-> change-set
(some? shape)
(conj {:type :mov-objects
:page-id page-id
:parent-id (:parent-id shape)
:shapes [id]
:after-shape prev-sibling
:index 0
:ignore-touched true}))))]
(-> changes
(update :redo-changes #(reduce add-redo-change % ids))
@@ -1150,3 +1150,24 @@
[changes]
(::page-id (meta changes)))
(defn set-text-content
[changes id content prev-content]
(assert-page-id! changes)
(let [page-id (::page-id (meta changes))
redo-change
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set :attr :content :val content}]}
undo-change
{:type :mod-obj
:page-id page-id
:id id
:operations [{:type :set :attr :content :val prev-content}]}]
(-> changes
(update :redo-changes conj redo-change)
(update :undo-changes conj undo-change))))

View File

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

View File

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

View File

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

View File

@@ -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 \
--add-modules java.base,jdk.management.agent,java.se,jdk.compiler,jdk.javadoc,jdk.attach,jdk.unsupported,jdk.jfr,jdk.jcmd \
--output /opt/jre;
FROM ubuntu:24.04 AS image

View File

@@ -30,11 +30,9 @@ x-uri: &penpot-public-uri
PENPOT_PUBLIC_URI: http://localhost:9001
x-body-size: &penpot-http-body-size
# 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)
# 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)
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_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" \
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" \
< /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_MULTIPART_BODY_SIZE;
client_max_body_size $PENPOT_HTTP_SERVER_MAX_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: 31457280
client_max_body_size 31457280;
# PENPOT_HTTP_SERVER_MAX_BODY_SIZE: 367001600
client_max_body_size 367001600;
# Logs: Configure your logs following the best practices inside your company
access_log /path/to/penpot.access.log;

View File

@@ -1,26 +0,0 @@
[
{
"~:features": {
"~#set": [
"variants/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type"
]
},
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true
},
"~:name": "Default",
"~:modified-at": "~m1713533116375",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f7920a",
"~:created-at": "~m1713533116375",
"~:is-default": true
}
]

View File

@@ -1,26 +0,0 @@
{
"~:email": "foo@example.com",
"~:is-demo": false,
"~:auth-backend": "penpot",
"~:fullname": "Princesa Leia",
"~:modified-at": "~m1713533116365",
"~:is-active": true,
"~:default-project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:id": "~uc7ce0794-0992-8105-8004-38e630f29a9b",
"~:is-muted": false,
"~:default-team-id": "~uc7ce0794-0992-8105-8004-38e630f40f6d",
"~:created-at": "~m1713533116365",
"~:is-blocked": false,
"~:props": {
"~:nudge": {
"~:big": 10,
"~:small": 1
},
"~:v2-info-shown": true,
"~:viewed-tutorial?": false,
"~:viewed-walkthrough?": false,
"~:onboarding-viewed": true,
"~:builtin-templates-collapsed-status":
true
}
}

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

@@ -0,0 +1,21 @@
{
"~: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

@@ -0,0 +1,131 @@
{
"~: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

@@ -1,4 +1,8 @@
export class BasePage {
static async init(page) {
await BasePage.mockConfigFlags(page, []);
}
/**
* Mocks multiple RPC calls in a single call.
*

View File

@@ -2,13 +2,8 @@ import { MockWebSocketHelper } from "../../helpers/MockWebSocketHelper";
import BasePage from "./BasePage";
export class BaseWebSocketPage extends BasePage {
/**
* This should be called on `test.beforeEach`.
*
* @param {Page} page
* @returns
*/
static async initWebSockets(page) {
static async init(page) {
await super.init(page);
await MockWebSocketHelper.init(page);
}

View File

@@ -3,54 +3,62 @@ import { BaseWebSocketPage } from "./BaseWebSocketPage";
export class DashboardPage extends BaseWebSocketPage {
static async init(page) {
await BaseWebSocketPage.initWebSockets(page);
await super.init(page);
await BaseWebSocketPage.mockRPC(
await super.mockConfigFlags(page, ["disable-onboarding"]);
await super.mockRPC(
page,
"get-teams",
"logged-in-user/get-teams-default.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-font-variants?team-id=*",
"workspace/get-font-variants-empty.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-projects?team-id=*",
"logged-in-user/get-projects-default.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-team-members?team-id=*",
"logged-in-user/get-team-members-your-penpot.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-team-users?team-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-unread-comment-threads?team-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-team-recent-files?team-id=*",
"logged-in-user/get-team-recent-files-empty.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-profiles-for-file-comments",
"workspace/get-profile-for-file-comments.json",
);
await BaseWebSocketPage.mockRPC(
await super.mockRPC(
page,
"get-builtin-templates",
"logged-in-user/get-built-in-templates-empty.json",
);
await super.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in.json",
);
}
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f40f6d";

View File

@@ -1,6 +1,10 @@
import { BasePage } from "./BasePage";
export class LoginPage extends BasePage {
static async init(page) {
await super.init(page);
}
constructor(page) {
super(page);
this.loginButton = page.getByRole("button", { name: "Continue" });

View File

@@ -29,8 +29,13 @@ export class RegisterPage extends BasePage {
);
}
static async init(page) {
await BasePage.init(page);
}
static async initWithLoggedOutUser(page) {
await this.mockRPC(page, "get-profile", "get-profile-anonymous.json");
await BasePage.init(page);
await BasePage.mockRPC(page, "get-profile", "get-profile-anonymous.json");
}
}

View File

@@ -3,9 +3,9 @@ import { DashboardPage } from "./DashboardPage";
export class SubscriptionProfilePage extends DashboardPage {
static async init(page) {
await DashboardPage.initWebSockets(page);
await super.init(page);
await DashboardPage.mockRPC(
await super.mockRPC(
page,
"get-subscription-usage",
"subscription/get-subscription-usage.json",

View File

@@ -4,16 +4,6 @@ export class ViewerPage extends BaseWebSocketPage {
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
/**
* This should be called on `test.beforeEach`.
*
* @param {Page} page
* @returns
*/
static async init(page) {
await BaseWebSocketPage.initWebSockets(page);
}
async setupLoggedInUser() {
await this.mockRPC(
"get-profile",

View File

@@ -45,24 +45,27 @@ export class WorkspacePage extends BaseWebSocketPage {
return this.waitForEditor();
}
stopEditing() {
return this.page.keyboard.press("Escape");
async stopEditing() {
await this.page.keyboard.press("Escape");
}
async moveToLeft(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowLeft");
}
await this.waitForIdle();
}
async moveToRight(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowRight");
}
await this.waitForIdle();
}
async moveFromStart(offset = 0) {
await this.page.keyboard.press("Home");
await this.waitForIdle();
await this.moveToRight(offset);
}
@@ -103,6 +106,10 @@ export class WorkspacePage extends BaseWebSocketPage {
changeLetterSpacing(newValue) {
return this.changeNumericInput(this.letterSpacing, newValue);
}
async waitForIdle() {
await this.page.evaluate(() => new Promise((resolve) => globalThis.requestIdleCallback(resolve)));
}
};
/**
@@ -112,9 +119,9 @@ export class WorkspacePage extends BaseWebSocketPage {
* @returns
*/
static async init(page) {
await BaseWebSocketPage.initWebSockets(page);
await super.init(page);
await BaseWebSocketPage.mockRPCs(page, {
await super.mockRPCs(page, {
"get-profile": "logged-in-user/get-profile-logged-in.json",
"get-team-users?file-id=*":
"logged-in-user/get-team-users-single-user.json",

View File

@@ -243,6 +243,46 @@ 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();

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test.describe("Dashboard Deleted Page", () => {

View File

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("BUG 10421 - Fix libraries context menu", async ({ page }) => {

View File

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("BUG 12359 - Selected invitations count is not pluralized", async ({

View File

@@ -3,11 +3,7 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
await DashboardPage.mockRPC(
page,
"get-teams",

View File

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("Dashboad page has title ", async ({ page }) => {

View File

@@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
test.beforeEach(async ({ page }) => {
await LoginPage.init(page);
const login = new LoginPage(page);
await login.initWithLoggedOutUser();

View File

@@ -4,6 +4,7 @@ import OnboardingPage from "../pages/OnboardingPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockConfigFlags(page, ["enable-onboarding"]);
await DashboardPage.mockRPC(
page,
"get-profile",

View File

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("Navigate to penpot changelog from profile menu", async ({ page }) => {

View File

@@ -2,8 +2,6 @@ import { test, expect } from "@playwright/test";
import { Clipboard } from "../../helpers/Clipboard";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
const timeToWait = 100;
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL);
@@ -37,11 +35,13 @@ test("Create a new text shape from pasting text", async ({ page, context }) => {
await workspace.setupEmptyFile();
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.moveButton.click();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("keyboard");
await workspace.textEditor.stopEditing();
await expect(workspace.layers.getByText(textToPaste)).toBeVisible();
@@ -57,6 +57,7 @@ test("Create a new text shape from pasting text using context menu", async ({
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.moveButton.click();
await Clipboard.writeText(page, textToPaste);
@@ -96,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");
@@ -139,7 +139,7 @@ test("Update a new text shape appending text by pasting text", async ({
await workspace.paste("keyboard");
await workspace.textEditor.stopEditing();
await workspace.waitForSelectedShapeName("Lorem ipsum dolor sit amet");
});
});
test.skip("Update a new text shape prepending text by pasting text", async ({
page,

View File

@@ -1,22 +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 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: 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();
@@ -44,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();
@@ -105,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();
@@ -169,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();
@@ -203,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" });
@@ -489,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();
@@ -540,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();
@@ -594,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();
@@ -648,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();
@@ -701,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
@@ -761,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
@@ -853,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,17 +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 WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-design-tokens-v1"]);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
@@ -159,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" })
@@ -321,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" });
@@ -466,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" });
@@ -602,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" });
@@ -718,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" });
@@ -832,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" });
@@ -1013,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
@@ -1048,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" });
@@ -1233,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" });
@@ -1480,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";
@@ -1530,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" })
@@ -1563,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
@@ -1590,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();
@@ -1614,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();
@@ -1643,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
@@ -1670,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();
@@ -1696,7 +1695,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFileRender(page);
await setupTypographyTokensFile(page);
await tokensSidebar
.getByRole("button")
@@ -1792,7 +1791,7 @@ test.describe("Tokens tab - edition", () => {
page,
}) => {
const { tokensUpdateCreateModal, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFileRender(page);
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
@@ -1828,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
@@ -1883,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();
@@ -1903,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,11 +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 WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-design-tokens-v1"]);
await WorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
});
@@ -43,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" })
@@ -140,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" })
@@ -177,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();
@@ -203,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");
@@ -220,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

@@ -1,5 +1,5 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { BaseWebSocketPage } from "../pages/BaseWebSocketPage";
import { Clipboard } from "../../helpers/Clipboard";
@@ -7,7 +7,7 @@ test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ALL);
await WasmWorkspacePage.init(page);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-variants.json");
await WasmWorkspacePage.mockConfigFlags(page, ["enable-feature-variants"]);
});
test.afterEach(async ({ context }) => {

View File

@@ -1,6 +1,5 @@
import { test, expect } from "@playwright/test";
import { WasmWorkspacePage } from "../pages/WasmWorkspacePage";
import { presenceFixture } from "../../data/workspace/ws-notifications";
test.beforeEach(async ({ page }) => {
await WasmWorkspacePage.init(page);
@@ -106,7 +105,7 @@ test("BUG 11006 - Fix history panel shortcut", async ({ page }) => {
await workspacePage.goToWorkspace();
await page.keyboard.press("Control+Alt+h");
await page.keyboard.press("ControlOrMeta+Alt+h");
await expect(
workspacePage.rightSidebar.getByText("There are no versions yet"),

View File

@@ -55,3 +55,31 @@ 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

@@ -3,11 +3,6 @@ import DashboardPage from "../pages/DashboardPage";
test.beforeEach(async ({ page }) => {
await DashboardPage.init(page);
await DashboardPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in-no-onboarding.json",
);
});
test("User goes to an empty dashboard", async ({ page }) => {

View File

@@ -2,6 +2,8 @@ import { test, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
test.beforeEach(async ({ page }) => {
await LoginPage.init(page);
const login = new LoginPage(page);
await login.initWithLoggedOutUser();
await login.page.goto("/#/auth/login");

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={{& ts}}" rel="stylesheet" type="text/css" />
<link href="css/ui.css?ts={{& version_tag}}" rel="stylesheet" type="text/css" />
{{#isDebug}}
<link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}}

View File

@@ -4,4 +4,9 @@ TARGET=${1:-app};
set -ex
exec pnpm run watch:$TARGET
rm -rf node_modules;
corepack enable;
corepack install;
pnpm install;
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") str))
(def mcp-help-center-uri "https://help.penpot.app/technical-guide/")
;; --- Helper Functions
(defn ^boolean check-browser? [candidate]

View File

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

View File

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

View File

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

View File

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

View File

@@ -79,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 structured-token->penpot-map (.-value token-symbol))
(mapv tokenscript-symbols->penpot-unit (.-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)])
@@ -88,7 +88,7 @@
(defn tokenscript-symbols->penpot-unit [^js v]
(cond
(structured-token? v) (structured-token->penpot-map v)
(list-symbol? v) (tokenscript-symbols->penpot-unit (.nth 1 v))
(list-symbol? v) (structured-token->penpot-map v)
(color-symbol? v) (.-value (.to v "hex"))
(rem-number-with-unit? v) (rem->px v)
:else (.-value v)))

View File

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

View File

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

View File

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

View File

@@ -1,60 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.mcp
(:require
[app.common.logging :as log]
[app.common.uri :as u]
[app.config :as cf]
[app.main.data.plugins :as dp]
[app.main.repo :as rp]
[app.plugins.register :refer [mcp-plugin-id]]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(log/set-level! :info)
(def ^:private default-manifest
{:code "plugin.js"
:name "Penpot MCP Plugin"
:version 2
:plugin-id mcp-plugin-id
:description "This plugin enables interaction with the Penpot MCP server"
:allow-background true
:permissions
#{"library:read" "library:write"
"comment:read" "comment:write"
"content:write" "content:read"}})
(defn init-mcp!
[]
(->> (rp/cmd! :get-current-mcp-token)
(rx/subs!
(fn [{:keys [token]}]
(when token
(dp/start-plugin!
(assoc default-manifest
:url (str (u/join cf/public-uri "plugins/mcp/manifest.json"))
:host (str (u/join cf/public-uri "plugins/mcp/")))
;; API extension for MCP server
#js {:mcp
#js
{:getToken (constantly token)
:getServerUrl #(str cf/mcp-ws-uri)
:setMcpStatus
(fn [status]
;; TODO: Visual feedback
(log/info :hint "MCP STATUS" :status status))}}))))))
(defn init-mcp-connexion
[]
(ptk/reify ::init-mcp-connexion
ptk/EffectEvent
(effect [_ state _]
(when (and (contains? cf/flags :mcp)
(-> state :profile :props :mcp-status))
(init-mcp!)))))

View File

@@ -197,11 +197,12 @@
objects (:objects page)
undo-id (or (:undo-id options) (js/Symbol))
[all-parents changes] (-> (pcb/empty-changes it (:id page))
(cls/generate-delete-shapes fdata page objects ids
{:ignore-touched (:allow-altering-copies options)
:undo-group (:undo-group options)
:undo-id undo-id}))]
[all-parents changes]
(-> (pcb/empty-changes it (:id page))
(cls/generate-delete-shapes fdata page objects ids
{:ignore-touched (:allow-altering-copies options)
:undo-group (:undo-group options)
:undo-id undo-id}))]
(rx/of (dwu/start-undo-transaction undo-id)
(dc/detach-comment-thread ids)

View File

@@ -10,6 +10,7 @@
[app.common.attrs :as attrs]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@@ -19,6 +20,7 @@
[app.common.types.shape.layout :as ctl]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.event :as ev]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.common :as dwc]
@@ -916,11 +918,11 @@
(update-in state [:workspace-text-modifier shape-id] {:position-data position-data}))))
(defn v2-update-text-shape-content
[id content & {:keys [update-name? name finalize? save-undo?]
:or {update-name? false name nil finalize? false save-undo? true}}]
[id content & {:keys [update-name? name finalize? save-undo? original-content]
:or {update-name? false name nil finalize? false save-undo? true original-content nil}}]
(ptk/reify ::v2-update-text-shape-content
ptk/WatchEvent
(watch [_ state _]
(watch [it state _]
(if (features/active-feature? state "render-wasm/v1")
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)
@@ -950,11 +952,11 @@
new-shape))
{:save-undo? save-undo? :undo-group (when new-shape? id)})
(let [modifiers (dwwt/resize-wasm-text-modifiers shape content)
options {:undo-group (when new-shape? id)}]
(if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers modifiers options)
(dwm/set-wasm-modifiers modifiers options))))
(when-let [modifiers (dwwt/resize-wasm-text-modifiers shape content)]
(let [options {:undo-group (when new-shape? id)}]
(if (and (not= :fixed (:grow-type shape)) finalize?)
(dwm/apply-wasm-modifiers modifiers options)
(dwm/set-wasm-modifiers modifiers options)))))
(when finalize?
(rx/concat
@@ -970,7 +972,13 @@
{:save-undo? false}))
(dws/deselect-shape id)
(dwsh/delete-shapes #{id})))
(rx/of (dwt/finish-transform))))))
(rx/of
;; This commit is necesary for undo and component propagation
;; on finalization
(dch/commit-changes
(-> (pcb/empty-changes it (:current-page-id state))
(pcb/set-text-content id content original-content)))
(dwt/finish-transform))))))
(let [objects (dsh/lookup-page-objects state)
shape (get objects id)

View File

@@ -27,27 +27,28 @@
(resize-wasm-text-modifiers shape (:content shape)))
([{:keys [id points selrect grow-type] :as shape} content]
(wasm.api/use-shape id)
(wasm.api/set-shape-text-content id content)
(wasm.api/set-shape-text-images id content)
(when id
(wasm.api/use-shape id)
(wasm.api/set-shape-text-content id content)
(wasm.api/set-shape-text-images id content)
(let [dimension (wasm.api/get-text-dimensions)
width-scale (if (#{:fixed :auto-height} grow-type)
1.0
(/ (:width dimension) (:width selrect)))
height-scale (if (= :fixed grow-type)
1.0
(/ (:height dimension) (:height selrect)))
resize-v (gpt/point width-scale height-scale)
origin (first points)]
(let [dimension (wasm.api/get-text-dimensions)
width-scale (if (#{:fixed :auto-height} grow-type)
1.0
(/ (:width dimension) (:width selrect)))
height-scale (if (= :fixed grow-type)
1.0
(/ (:height dimension) (:height selrect)))
resize-v (gpt/point width-scale height-scale)
origin (first points)]
{id
{:modifiers
(ctm/resize-modifiers
resize-v
origin
(:transform shape (gmt/matrix))
(:transform-inverse shape (gmt/matrix)))}})))
{id
{:modifiers
(ctm/resize-modifiers
resize-v
origin
(:transform shape (gmt/matrix))
(:transform-inverse shape (gmt/matrix)))}}))))
(defn resize-wasm-text
"Resize a single text shape (auto-width/auto-height) by id.

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]}]
@@ -305,7 +299,7 @@
(if (:nitrate-licence profile)
;; TODO update when org creation route is ready
(dom/open-new-window "/control-center/org/create")
(st/emit! (dnt/show-nitrate-popup)))))]
(st/emit! (modal/show :nitrate-form {})))))]
[:> dropdown-menu* props
@@ -503,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)
@@ -541,53 +530,36 @@
(dom/prevent-default event)
(dom/stop-propagation event)
(some-> (dom/get-current-target event)
(dom/click)))))
(dom/click!)))))
close-teams-menu
(mf/use-fn #(reset! show-teams-menu* false))
(mf/use-fn #(reset! show-teams-menu* false))]
on-create-org-click
(mf/use-fn
(fn []
(if (:nitrate-licence profile)
;; TODO update when org creation route is ready
(dom/open-new-window "/control-center/org/create")
(st/emit! (dnt/show-nitrate-popup)))))]
(if empty?
[:div {:class (stl/css :nitrate-orgs-empty)}
[:span {:class (stl/css :nitrate-penpot-icon)}
[:> raw-svg* {:id penpot-logo-icon}]]
"Penpot"
[:> button* {:variant "ghost"
:type "button"
:class (stl/css :nitrate-create-org)
:on-click on-create-org-click} (tr "dashboard.create-new-org")]]
[:div {:class (stl/css :sidebar-team-switch)}
[:div {:class (stl/css :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]}]
@@ -629,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))
@@ -649,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))]
@@ -733,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)))
@@ -823,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
@@ -1087,13 +1056,10 @@
(dom/open-new-window "https://penpot.app/pricing")))]
[:*
(if (contains? cf/flags :nitrate)
(when-not (:nitrate-licence profile)
[:> nitrate-sidebar* {:profile profile}])
(when (contains? cf/flags :subscriptions)
(if (show-subscription-dashboard-banner? profile)
[:> dashboard-cta* {:profile profile}]
[:> subscription-sidebar* {:profile profile}])))
(when (contains? cf/flags :subscriptions)
(if (show-subscription-dashboard-banner? profile)
[:> dashboard-cta* {:profile profile}]
[:> subscription-sidebar* {:profile profile}]))
;; TODO remove this block when subscriptions is full implemented
(when (contains? cf/flags :subscriptions-old)

View File

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

View File

@@ -6,7 +6,6 @@
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.event :as ev]
[app.main.data.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))))]
;; TODO add translations for this texts when we have the definitive ones
[:div {:class (stl/css :nitrate-banner :highlighted)}
[:div {:class (stl/css :nitrate-content)}
[:span {:class (stl/css :nitrate-title)} "Unlock Nitrate features"]]
[:div {:class (stl/css :nitrate-content)}
[:span {:class (stl/css :nitrate-info)} "Some further information and explanation."]
[:> button* {:variant "primary"
:type "button"
:class (stl/css :cta-bottom-button)
:on-click handle-click} "UPGRADE TO NITRATE"]]]))
(mf/defc team*
[{:keys [is-owner team]}]
(let [subscription (:subscription team)

View File

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

View File

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

View File

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

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