Compare commits

..

36 Commits

Author SHA1 Message Date
Andrey Antukh
347a758831 WIP 2025-12-11 12:22:28 +01:00
Andrey Antukh
4aea00a76f WIP 2025-12-11 12:16:39 +01:00
Andrey Antukh
5a260294a1 🔧 Update build-tag.yml github workflow 2025-12-11 12:00:42 +01:00
Andrey Antukh
3f6e44316e 🐛 Add missing node depes install on render-wasm 2025-12-11 11:51:47 +01:00
Eva Marco
77ef8e6fe6 🐛 Fix scroll on move library modal (#7952) 2025-12-11 10:46:54 +01:00
Alejandro Alonso
916b7709dc Update Pencil Penpot Design System System template in carousel (#7948) 2025-12-10 15:09:28 +01:00
Eva Marco
443e41fea4 🐛 Fix multiple selection with color tokens (#7941) 2025-12-10 14:36:08 +01:00
Alejandro Alonso
c7c9b04095 Merge pull request #7944 from penpot/niwinz-staging-exporter-fix
🐛 Fix incorrect resource lifetime handling on exporter
2025-12-10 14:35:20 +01:00
Eva Marco
c61a0c0332 📚 Add line to changelog (#7945) 2025-12-10 13:58:18 +01:00
Eva Marco
8707ff6511 🎉 Add spanish translation 2025-12-10 13:12:30 +01:00
Florian Schroedl
3d8a251741 🐛 Disallow font-family referencing composite token 2025-12-10 13:12:30 +01:00
Andrey Antukh
34e84ee3c8 🐛 Fix incorrect resource lifetime handling on exporter 2025-12-10 13:02:31 +01:00
Alejandro Alonso
e8201402a7 Merge pull request #7938 from penpot/niwinz-staging-bugfix-5
🐛 Fix several issues
2025-12-10 12:05:42 +01:00
Aitor Moreno
8a22477b96 Merge pull request #7932 from penpot/niwinz-staging-worker-wasm-load
🐛 Fix WASM loading strategy on worker
2025-12-10 11:47:31 +01:00
Alejandro Alonso
3e684ea54f ⬆️ Update svgo dependency on frontend (#7936) 2025-12-10 10:07:02 +01:00
Andrey Antukh
98039f13d8 🐛 Fix main toolbar z-index 2025-12-10 09:47:40 +01:00
Alejandro Alonso
40c27591f6 🐛 Fix svg import (#7925) 2025-12-10 08:36:54 +01:00
Andrey Antukh
91d20a46d1 💄 Add cosmetic changes to exports assets progress component 2025-12-10 08:23:05 +01:00
Andrey Antukh
50bead7c56 🐛 Fix react warning on having p inside p on assets export progress 2025-12-10 08:22:41 +01:00
Andrey Antukh
b75b999903 📎 Fix devenv jvm warning 2025-12-10 08:22:05 +01:00
Andrey Antukh
810f1721c8 🐛 Fix recursion render on subscription modal 2025-12-10 07:54:52 +01:00
Andrey Antukh
a4646373cf ♻️ Refactor wasm loading strategy on worker 2025-12-09 19:41:19 +01:00
Andrey Antukh
f111cbb2a4 Add better cache config on devenv nginx 2025-12-09 19:38:30 +01:00
Aitor Moreno
a614207f7e 🐛 Fix exporter failing with HTTPS 2025-12-09 16:08:20 +01:00
Luis de Dios
6ce3249c6d 🐛 Fix color format does not switch in the view mode (#7923)
* 🐛 Fix color format does not switch in the inspect mode of the view mode

* ♻️ Update components
2025-12-09 14:38:15 +01:00
Pablo Alba
b0351be724 🐛 Fix switch variants with paths 2025-12-09 11:08:55 +01:00
Andrey Antukh
b8392b3731 🐛 Fix regression on sending team invitations (#7912) 2025-12-05 12:36:06 +01:00
Andrey Antukh
77dba477ca 🔧 Backport build-tag github workflow from develop 2025-12-05 10:25:03 +01:00
Eva Marco
b6598d1f07 🐛 Fix scrollbar on color modal (#7906) 2025-12-05 09:55:41 +01:00
Xaviju
bf1dc21c75 💄 Hide themes & sets panels when none active (#7902) 2025-12-04 14:11:57 +01:00
Alejandro Alonso
46c20a993f Merge pull request #7904 from penpot/niwinz-staging-fix-invitation-resend
🐛 Fix exception on resending invitation
2025-12-04 11:56:07 +01:00
Andrey Antukh
0e0106f69a 🐛 Add correct assertion on create-invitation fn 2025-12-04 11:38:32 +01:00
Andrey Antukh
19bb69cc60 Improve invalid schema error report 2025-12-04 11:38:16 +01:00
Alejandro Alonso
504eb70988 Merge pull request #7885 from penpot/niwinz-staging-bugfix-2
🐛 Make workspace palette reposition on left sidebar collapse
2025-12-04 11:19:20 +01:00
Andrey Antukh
feababe2a8 🐛 Make workspace palette reposition on left sidebar collapse 2025-12-03 09:56:14 +01:00
Andrey Antukh
5ef06685fc 💄 Add cosmetic improvements to workspace palette component 2025-12-03 09:38:23 +01:00
120 changed files with 13861 additions and 17353 deletions

View File

@@ -11,7 +11,7 @@ jobs:
secrets: inherit
with:
gh_ref: ${{ github.ref_name }}
build_wasm: "no"
build_wasm: "yes"
build_storybook: "yes"
build-docker:
@@ -21,6 +21,22 @@ jobs:
with:
gh_ref: ${{ github.ref_name }}
notify:
name: Notifications
runs-on: ubuntu-24.04
needs: build-docker
steps:
- name: Notify Mattermost
uses: mattermost/action-mattermost-notify@master
with:
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
MATTERMOST_CHANNEL: bot-alerts-cicd
TEXT: |
🐳 *[PENPOT] Docker image available.*
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
@infra
publish-final-tag:
if: ${{ !contains(github.ref_name, '-RC') && !contains(github.ref_name, '-alpha') && !contains(github.ref_name, '-beta') && contains(github.ref_name, '.') }}
needs: build-docker

View File

@@ -92,6 +92,8 @@ example. It's still usable as before, we just removed the example.
- Fix U and E icon displayed in project list [Taiga #12806](https://tree.taiga.io/project/penpot/issue/12806)
- Fix unpublish library modal not scrolling a long file list [Taiga #12285](https://tree.taiga.io/project/penpot/issue/12285)
- Fix incorrect interaction betwen hower and scroll on assets sidebar [Taiga #12389](https://tree.taiga.io/project/penpot/issue/12389)
- Fix switch variants with paths [Taiga #12841](https://tree.taiga.io/project/penpot/issue/12841)
- Fix referencing typography tokens on font-family tokens [Taiga #12492](https://tree.taiga.io/project/penpot/issue/12492)
## 2.11.1

View File

@@ -3,7 +3,7 @@
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Tokens%20starter%20kit.penpot"}
{:id "penpot-design-system"
:name "Penpot Design System | Pencil"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/penpot-app.penpot"}
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Pencil-Penpot-Design-System.penpot"}
{:id "wireframing-kit"
:name "Wireframe library"
:file-uri "https://github.com/penpot/penpot-files/raw/refs/heads/main/Wireframing%20kit%20v1.1.penpot"}

View File

@@ -12,8 +12,11 @@
[app.common.files.changes-builder :as pcb]
[app.common.files.helpers :as cfh]
[app.common.files.variant :as cfv]
[app.common.geom.matrix :as gmt]
[app.common.geom.point :as gpt]
[app.common.geom.rect :as grc]
[app.common.geom.shapes :as gsh]
[app.common.geom.shapes.common :as gco]
[app.common.logging :as log]
[app.common.logic.shapes :as cls]
[app.common.logic.variant-properties :as clvp]
@@ -26,6 +29,7 @@
[app.common.types.library :as ctl]
[app.common.types.page :as ctp]
[app.common.types.pages-list :as ctpl]
[app.common.types.path.segment :as segment]
[app.common.types.shape :as cts]
[app.common.types.shape-tree :as ctst]
[app.common.types.shape.interactions :as ctsi]
@@ -1876,6 +1880,44 @@
roperations'
uoperations')))))))
(defn- set-path-new-values
[current-shape prev-shape transform]
(let [new-content (segment/transform-content
(:content current-shape)
(gmt/transform-in (gpt/point 0 0) transform))
new-points (-> (segment/content->selrect new-content)
(grc/rect->points))
points-center (gco/points->center new-points)
new-selrect (gsh/calculate-selrect new-points points-center)
shape (assoc current-shape
:content new-content
:points new-points
:selrect new-selrect)
prev-center (segment/content-center (:content prev-shape))
delta (gpt/subtract points-center (first new-points))
new-pos (gpt/subtract prev-center delta)]
(gsh/absolute-move shape new-pos)))
(defn- switch-path-change-value
[prev-shape ;; The shape before the switch
current-shape ;; The shape after the switch (a clean copy)
ref-shape ;; The referenced shape on the main component
;; before the switch
attr]
(let [old-width (-> ref-shape :selrect :width)
new-width (-> prev-shape :selrect :width)
old-height (-> ref-shape :selrect :height)
new-height (-> prev-shape :selrect :height)
transform (-> (gpt/point (/ new-width old-width)
(/ new-height old-height))
(gmt/scale-matrix))
shape (set-path-new-values current-shape prev-shape transform)]
(get shape attr)))
(defn- switch-text-change-value
[prev-content ;; The :content of the text before the switch
@@ -2027,6 +2069,10 @@
(= :content attr)
(touched attr-group))
path-change?
(and (= :path (:type current-shape))
(contains? #{:points :selrect :content} attr))
;; position-data is a special case because can be affected by :geometry-group and :content-group
;; so, if the position-data changes but the geometry is touched we need to reset the position-data
;; so it's calculated again
@@ -2055,6 +2101,12 @@
(:content origin-ref-shape)
touched)
path-change?
(switch-path-change-value previous-shape
current-shape
origin-ref-shape
attr)
:else
(get previous-shape attr)))

View File

@@ -281,7 +281,20 @@
(defn check-fn
"Create a predefined check function"
[s & {:keys [hint type code]}]
(let [s (schema s)
(let [s #?(:clj
(schema s)
:cljs
(try
(schema s)
(catch :default cause
(let [data (ex-data cause)]
(if (= :malli.core/invalid-schema (:type data))
(throw (ex-info
(str "Invalid schema\n"
(pp/pprint-str (:data data)))
{}))
(throw cause))))))
validator* (delay (m/validator s))
explainer* (delay (m/explainer s))
hint (or ^boolean hint "check error")

View File

@@ -234,15 +234,16 @@
"Calculate the boolean content from shape and objects. Returns a
packed PathData instance"
[shape objects]
(let [content (calc-bool-content* shape objects)]
(let [content (if (fn? wasm:calc-bool-content)
(wasm:calc-bool-content (get shape :bool-type)
(get shape :shapes))
(calc-bool-content* shape objects))]
(impl/path-data content)))
(defn update-bool-shape
"Calculates the selrect+points for the boolean shape"
[shape objects]
(let [content (if (fn? wasm:calc-bool-content)
(wasm:calc-bool-content shape objects)
(calc-bool-content shape objects))
(let [content (calc-bool-content shape objects)
shape (assoc shape :content content)]
(update-geometry shape)))

View File

@@ -223,16 +223,19 @@ http {
add_header X-Cache-Status $upstream_cache_status;
}
location ~ ^/(/|css|fonts|images|js|wasm|mjs|map) {
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
add_header Cache-Control "public, max-age=604800" always; # 7 days
}
location ~* \.(js|css|wasm)$ {
add_header Cache-Control "no-store" always;
}
location ~ ^/[^/]+/(.*)$ {
return 301 " /404";
}
add_header Cache-Control "no-store";
# This header is what we need to use on prod
# add_header Cache-Control "public, must-revalidate, max-age=0";
add_header Cache-Control "no-store" always;
try_files $uri /index.html$is_args$args /index.html =404;
}
}

View File

@@ -21,6 +21,7 @@
"raw-body": "^3.0.1",
"source-map-support": "^0.5.21",
"svgo": "penpot/svgo#v3.1",
"undici": "^7.16.0",
"xml-js": "^1.6.11",
"xregexp": "^5.1.2"
},

View File

@@ -100,7 +100,7 @@
(def browser-pool-factory
(letfn [(create []
(p/let [opts #js {:args #js ["--font-render-hinting=none"]}
(p/let [opts #js {:args #js ["--allow-insecure-localhost" "--font-render-hinting=none"]}
browser (.launch pw/chromium opts)
id (swap! pool-browser-id inc)]
(l/info :origin "factory" :action "create" :browser-id id)

View File

@@ -74,7 +74,7 @@
(p/fmap (fn [resource]
(assoc exchange :response/body resource)))
(p/merr (fn [cause]
(l/error :hint "unexpected error on export multiple"
(l/error :hint "unexpected error on single export"
:cause cause)
(p/rejected cause))))))
@@ -94,7 +94,7 @@
(redis/pub! topic data))))
on-error (fn [cause]
(l/error :hint "unexpected error on multiple exportation" :cause cause)
(l/error :hint "unexpected error on multiple export" :cause cause)
(if wait
(p/rejected cause)
(redis/pub! topic {:type :export-update
@@ -107,12 +107,12 @@
:on-progress on-progress)
append (fn [{:keys [filename path] :as resource}]
(rsc/add-to-zip! zip path (str/replace filename sanitize-file-regex "_")))
(rsc/add-to-zip zip path (str/replace filename sanitize-file-regex "_")))
proc (->> exports
(map (fn [export] (rd/render export append)))
(p/all)
(p/fnly (fn [_] (.finalize zip)))
(p/mcat (fn [_] (rsc/close-zip zip)))
(p/fmap (constantly resource))
(p/mcat (partial rsc/upload-resource auth-token))
(p/fmap (fn [resource]

View File

@@ -11,6 +11,7 @@
["node:fs" :as fs]
["node:fs/promises" :as fsp]
["node:path" :as path]
["undici" :as http]
[app.common.exceptions :as ex]
[app.common.transit :as t]
[app.common.uri :as u]
@@ -53,30 +54,40 @@
(.pipe zip out)
zip))
(defn add-to-zip!
(defn add-to-zip
[zip path name]
(.file ^js zip path #js {:name name}))
(defn close-zip!
(defn close-zip
[zip]
(.finalize ^js zip))
(p/create (fn [resolve]
(.on ^js zip "close" resolve)
(.finalize ^js zip))))
(defn upload-resource
[auth-token resource]
(->> (fsp/readFile (:path resource))
(p/fmap (fn [buffer]
(js/console.log buffer)
(new js/Blob #js [buffer] #js {:type (:mtype resource)})))
(p/mcat (fn [blob]
(let [fdata (new js/FormData)
uri (-> (cf/get :public-uri)
(u/ensure-path-slash)
(u/join "api/management/methods/upload-tempfile")
(str))]
(let [fdata (new http/FormData)
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
headers #js {"X-Shared-Key" cf/management-key
"Authorization" (str "Bearer " auth-token)}
request #js {:headers headers
:method "POST"
:body fdata
:dispatcher agent}
uri (-> (cf/get :public-uri)
(u/ensure-path-slash)
(u/join "api/management/methods/upload-tempfile")
(str))]
(.append fdata "content" blob (:filename resource))
(js/fetch uri #js {:headers #js {"X-Shared-Key" cf/management-key
"Authorization" (str "Bearer " auth-token)}
:method "POST"
:body fdata}))))
(http/fetch uri request))))
(p/mcat (fn [response]
(if (not= (.-status response) 200)
(ex/raise :type :internal

View File

@@ -75,7 +75,8 @@
[path]
(->> (.stat fs/promises path)
(p/fmap (fn [data]
{:created-at (inst-ms (.-ctime ^js data))
{:path path
:created-at (inst-ms (.-ctime ^js data))
:size (.-size data)}))
(p/merr (fn [_cause]
(p/resolved nil)))))

View File

@@ -582,6 +582,7 @@ __metadata:
raw-body: "npm:^3.0.1"
source-map-support: "npm:^0.5.21"
svgo: "penpot/svgo#v3.1"
undici: "npm:^7.16.0"
ws: "npm:^8.18.3"
xml-js: "npm:^1.6.11"
xregexp: "npm:^5.1.2"
@@ -1513,6 +1514,13 @@ __metadata:
languageName: node
linkType: hard
"undici@npm:^7.16.0":
version: 7.16.0
resolution: "undici@npm:7.16.0"
checksum: 10c0/efd867792e9f233facf9efa0a087e2d9c3e4415c0b234061b9b40307ca4fa01d945fee4d43c7b564e1b80e0d519bcc682f9f6e0de13c717146c00a80e2f1fb0f
languageName: node
linkType: hard
"unique-filename@npm:^4.0.0":
version: 4.0.0
resolution: "unique-filename@npm:4.0.0"

View File

@@ -50,5 +50,8 @@
:shadow-cljs
{:main-opts ["-m" "shadow.cljs.devtools.cli"]
:jvm-opts ["--sun-misc-unsafe-memory-access=allow" "-Dpenpot.wasm.profile-marks=true"]}
:jvm-opts ["--sun-misc-unsafe-memory-access=allow"
"-Dpenpot.wasm.profile-marks=true"
"-XX:+UnlockExperimentalVMOptions"
"-XX:CompileCommand=blackhole,criterium.blackhole.Blackhole::consume"]}
}}

View File

@@ -45,9 +45,9 @@
"translations": "node ./scripts/translations.js",
"watch:app:assets": "node ./scripts/watch.js",
"watch:app:libs": "node ./scripts/build-libs.js --watch",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main storybook",
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
"clear:shadow-cache": "rm -rf .shadow-cljs",
"watch:app": "yarn run clear:shadow-cache && yarn run build:app:worker && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch:app": "yarn run clear:shadow-cache && concurrently \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
"watch": "yarn run watch:app:assets",
"watch:storybook": "yarn run build:storybook:assets && concurrently \"storybook dev -p 6006 --no-open\" \"yarn run watch:storybook:assets\"",
"watch:storybook:assets": "node ./scripts/watch-storybook.js"
@@ -106,7 +106,7 @@
"@penpot/hljs": "portal:./vendor/hljs",
"@penpot/mousetrap": "portal:./vendor/mousetrap",
"@penpot/plugins-runtime": "1.3.2",
"@penpot/svgo": "penpot/svgo#v3.1",
"@penpot/svgo": "penpot/svgo#v3.2",
"@penpot/text-editor": "portal:./text-editor",
"@tokens-studio/sd-transforms": "1.2.11",
"@zip.js/zip.js": "patch:@zip.js/zip.js@npm%3A2.7.60#~/.yarn/patches/@zip.js-zip.js-npm-2.7.60-b6b814410b.patch",

View File

@@ -1,58 +0,0 @@
{
"~:features": {
"~#set": [
"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,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "New File 1",
"~:revn": 11,
"~:modified-at": "~m1713873823633",
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:is-shared": false,
"~:version": 46,
"~:project-id": "~uc7ce0794-0992-8105-8004-38e630f7920b",
"~:created-at": "~m1713536343369",
"~:data": {
"~:pages": [
"~u66697432-c33d-8055-8006-2c62cc084cad"
],
"~:pages-index": {
"~u66697432-c33d-8055-8006-2c62cc084cad": {
"~#penpot/pointer": [
"~ude58c8f6-c5c2-8196-8004-3df9e2e52d88",
{
"~:created-at": "~m1713873823636"
}
]
}
},
"~:id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:options": {
"~:components-v2": true
},
"~:recent-colors": [
{
"~:color": "#0000ff",
"~:opacity": 1,
"~:id": null,
"~:file-id": null,
"~:image": null
}
]
}
}

View File

@@ -1,345 +0,0 @@
{
"~:features": {
"~#set": [
"fdata/path-data",
"plugins/runtime",
"design-tokens/v1",
"layout/grid",
"styles/v2",
"fdata/pointer-map",
"fdata/objects-map",
"components/v2",
"fdata/shape-data-type",
"text-editor/v2"
]
},
"~:team-id": "~u9e6e22b2-db76-81d6-8006-75d7cdbb8bad",
"~:permissions": {
"~:type": "~:membership",
"~:is-owner": true,
"~:is-admin": true,
"~:can-edit": true,
"~:can-read": true,
"~:is-logged": true
},
"~:has-media-trimmed": false,
"~:comment-thread-seqn": 0,
"~:name": "Bug 11552",
"~:revn": 3,
"~:modified-at": "~m1753957736516",
"~:vern": 0,
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~: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",
"0004-clean-shadow-color",
"0005-deprecate-image-type",
"0006-fix-old-texts-fills",
"0007-clear-invalid-strokes-and-fills-v2",
"0008-fix-library-colors-v4",
"0009-clean-library-colors",
"0009-add-partial-text-touched-flags"
]
},
"~:version": 67,
"~:project-id": "~u9e6e22b2-db76-81d6-8006-75d7cdc30669",
"~:created-at": "~m1753957644225",
"~:data": {
"~:pages": ["~u238a17e0-75ff-8075-8006-934586ea2231"],
"~:pages-index": {
"~u238a17e0-75ff-8075-8006-934586ea2231": {
"~:objects": {
"~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
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.0
}
},
{
"~#point": {
"~:x": 0.01,
"~:y": 0.01
}
},
{
"~#point": {
"~:x": 0.0,
"~:y": 0.01
}
}
],
"~:r2": 0,
"~:proportion-lock": false,
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:r3": 0,
"~:r1": 0,
"~:id": "~u00000000-0000-0000-0000-000000000000",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:strokes": [],
"~:x": 0,
"~:proportion": 1.0,
"~:r4": 0,
"~:selrect": {
"~#rect": {
"~:x": 0,
"~:y": 0,
"~:width": 0.01,
"~:height": 0.01,
"~:x1": 0,
"~:y1": 0,
"~:x2": 0.01,
"~:y2": 0.01
}
},
"~:fills": [
{
"~:fill-color": "#FFFFFF",
"~:fill-opacity": 1
}
],
"~:flip-x": null,
"~:height": 0.01,
"~:flip-y": null,
"~:shapes": ["~ucc6f0580-449c-8019-8006-9345db077fa0"]
}
},
"~ucc6f0580-449c-8019-8006-9345db077fa0": {
"~#shape": {
"~:y": 438,
"~:transform": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:rotation": 0,
"~:grow-type": "~:auto-width",
"~:content": {
"~:type": "root",
"~:key": "1s4am1jl24s",
"~:children": [
{
"~:type": "paragraph-set",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:children": [
{
"~:line-height": "1.2",
"~:font-style": "normal",
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:font-id": "sourcesanspro",
"~:key": "13p0zwl2yhc",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro",
"~:text": "Lorem ipsum"
}
],
"~:typography-ref-id": null,
"~:text-transform": "none",
"~:text-align": "left",
"~:font-id": "sourcesanspro",
"~:key": "20hf3kmyoub",
"~:font-size": "14",
"~:font-weight": "400",
"~:typography-ref-file": null,
"~:text-direction": "ltr",
"~:type": "paragraph",
"~:font-variant-id": "regular",
"~:text-decoration": "none",
"~:letter-spacing": "0",
"~:fills": [
{
"~:fill-color": "#000000",
"~:fill-opacity": 1
}
],
"~:font-family": "sourcesanspro"
}
]
}
],
"~:vertical-align": "top"
},
"~:hide-in-viewer": false,
"~:name": "Lorem ipsum",
"~:width": 77,
"~:type": "~:text",
"~:points": [
{
"~#point": {
"~:x": 404,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 438
}
},
{
"~#point": {
"~:x": 481,
"~:y": 455
}
},
{
"~#point": {
"~:x": 404,
"~:y": 455
}
}
],
"~:transform-inverse": {
"~#matrix": {
"~:a": 1.0,
"~:b": 0.0,
"~:c": 0.0,
"~:d": 1.0,
"~:e": 0.0,
"~:f": 0.0
}
},
"~:id": "~ucc6f0580-449c-8019-8006-9345db077fa0",
"~:parent-id": "~u00000000-0000-0000-0000-000000000000",
"~:frame-id": "~u00000000-0000-0000-0000-000000000000",
"~:x": 404,
"~:selrect": {
"~#rect": {
"~:x": 404,
"~:y": 438,
"~:width": 77,
"~:height": 17,
"~:x1": 404,
"~:y1": 438,
"~:x2": 481,
"~:y2": 455
}
},
"~:flip-x": null,
"~:height": 17,
"~:flip-y": null
}
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2231",
"~:name": "Page 1"
}
},
"~:id": "~u238a17e0-75ff-8075-8006-934586ea2230",
"~:options": {
"~:components-v2": true,
"~:base-font-size": "16px"
}
}
}

View File

@@ -1 +1,5 @@
w
{
"~:revn": 2,
"~:lagged": []
}

View File

@@ -1,4 +0,0 @@
{
"~:revn": 2,
"~:lagged": []
}

View File

@@ -1,9 +0,0 @@
[
{
"~:id": "~u088df3d4-d383-80f6-8004-527e50ea4f1f",
"~:revn": 21,
"~:file-id": "~uc7ce0794-0992-8105-8004-38f280443849",
"~:session-id": "~u1dc6d4fa-7bd3-803a-8004-527dd9df2c62",
"~:changes": []
}
]

View File

@@ -1,36 +0,0 @@
export class Clipboard {
static Permission = {
ONLY_READ: ['clipboard-read'],
ONLY_WRITE: ['clipboard-write'],
ALL: ['clipboard-read', 'clipboard-write']
}
static enable(context, permissions) {
return context.grantPermissions(permissions)
}
static writeText(page, text) {
return page.evaluate((text) => navigator.clipboard.writeText(text), text);
}
static readText(page) {
return page.evaluate(() => navigator.clipboard.readText());
}
constructor(page, context) {
this.page = page
this.context = context
}
enable(permissions) {
return Clipboard.enable(this.context, permissions);
}
writeText(text) {
return Clipboard.writeText(this.page, text);
}
readText() {
return Clipboard.readText(this.page);
}
}

View File

@@ -1,30 +0,0 @@
export class Transit {
static parse(value) {
if (typeof value !== 'string')
return value
if (value.startsWith('~'))
return value.slice(2)
return value
}
static get(object, ...path) {
let aux = object;
for (const name of path) {
if (typeof name !== 'string') {
if (!(name in aux)) {
return undefined;
}
aux = aux[name];
} else {
const transitName = `~:${name}`;
if (!(transitName in aux)) {
return undefined;
}
aux = aux[transitName];
}
}
return this.parse(aux);
}
}

View File

@@ -1,27 +1,4 @@
export class BasePage {
/**
* Mocks multiple RPC calls in a single call.
*
* @param {Page} page
* @param {object<string, string>} paths
* @param {*} options
* @returns {Promise<void>}
*/
static async mockRPCs(page, paths, options) {
for (const [path, jsonFilename] of Object.entries(paths)) {
await this.mockRPC(page, path, jsonFilename, options)
}
}
/**
* Mocks an RPC call using a file.
*
* @param {Page} page
* @param {string} path
* @param {string} jsonFilename
* @param {*} options
* @returns {Promise<void>}
*/
static async mockRPC(page, path, jsonFilename, options) {
if (!page) {
throw new TypeError("Invalid page argument. Must be a Playwright page.");
@@ -116,10 +93,6 @@ export class BasePage {
return this.#page;
}
async mockRPCs(paths, options) {
return BasePage.mockRPCs(this.page, paths, options);
}
async mockRPC(path, jsonFilename, options) {
return BasePage.mockRPC(this.page, path, jsonFilename, options);
}

View File

@@ -1,146 +1,7 @@
import { expect } from "@playwright/test";
import { readFile } from 'node:fs/promises';
import { BaseWebSocketPage } from "./BaseWebSocketPage";
import { Transit } from '../../helpers/Transit';
export class WorkspacePage extends BaseWebSocketPage {
static TextEditor = class TextEditor {
constructor(workspacePage) {
this.workspacePage = workspacePage;
// locators.
this.fontSize = this.workspacePage.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
this.lineHeight = this.workspacePage.rightSidebar.getByRole("textbox", {
name: "Line Height",
});
this.letterSpacing = this.workspacePage.rightSidebar.getByRole(
"textbox",
{
name: "Letter Spacing",
},
);
}
get page() {
return this.workspacePage.page;
}
async waitForStyle(locator, styleName) {
return locator.evaluate(
(element, styleName) => element.style.getPropertyValue(styleName),
styleName,
);
}
async waitForEditor() {
return this.page.waitForSelector('[data-itype="editor"]');
}
async waitForRoot() {
return this.page.waitForSelector('[data-itype="root"]');
}
async waitForParagraph(nth) {
if (!nth) {
return this.page.waitForSelector('[data-itype="paragraph"]');
}
return this.page.waitForSelector(
`[data-itype="paragraph"]:nth-child(${nth})`,
);
}
async waitForParagraphStyle(nth, styleName) {
const paragraph = await this.waitForParagraph(nth);
return this.waitForStyle(paragraph, styleName);
}
async waitForTextSpan(nth = 0) {
if (!nth) {
return this.page.waitForSelector('[data-itype="inline"]');
}
return this.page.waitForSelector(
`[data-itype="inline"]:nth-child(${nth})`,
);
}
async waitForTextSpanContent(nth = 0) {
const textSpan = await this.waitForTextSpan(nth);
const textContent = await textSpan.textContent();
return textContent;
}
async waitForTextSpanStyle(nth, styleName) {
const textSpan = await this.waitForTextSpan(nth);
return this.waitForStyle(textSpan, styleName);
}
async startEditing() {
await this.page.keyboard.press("Enter");
return this.waitForEditor();
}
stopEditing() {
return this.page.keyboard.press("Escape");
}
async moveToLeft(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowLeft");
}
}
async moveToRight(amount = 0) {
for (let i = 0; i < amount; i++) {
await this.page.keyboard.press("ArrowRight");
}
}
async moveFromStart(offset = 0) {
await this.page.keyboard.press("ArrowLeft");
await this.moveToRight(offset);
}
async moveFromEnd(offset = 0) {
await this.page.keyboard.press("ArrowRight");
await this.moveToLeft(offset);
}
async selectFromStart(length, offset = 0) {
await this.moveFromStart(offset);
await this.page.keyboard.down("Shift");
await this.moveToRight(length);
await this.page.keyboard.up("Shift");
}
async selectFromEnd(length, offset = 0) {
await this.moveFromEnd(offset);
await this.page.keyboard.down("Shift");
await this.moveToLeft(length);
await this.page.keyboard.up("Shift");
}
async changeNumericInput(locator, newValue) {
await expect(locator).toBeVisible();
await locator.focus();
await locator.fill(`${newValue}`);
await locator.blur();
}
changeFontSize(newValue) {
return this.changeNumericInput(this.fontSize, newValue);
}
changeLineHeight(newValue) {
return this.changeNumericInput(this.lineHeight, newValue);
}
changeLetterSpacing(newValue) {
return this.changeNumericInput(this.letterSpacing, newValue);
}
};
/**
* This should be called on `test.beforeEach`.
*
@@ -150,21 +11,50 @@ export class WorkspacePage extends BaseWebSocketPage {
static async init(page) {
await BaseWebSocketPage.initWebSockets(page);
await BaseWebSocketPage.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",
"get-comment-threads?file-id=*":
"workspace/get-comment-threads-empty.json",
"get-project?id=*": "workspace/get-project-default.json",
"get-team?id=*": "workspace/get-team-default.json",
"get-teams": "get-teams.json",
"get-team-members?team-id=*":
"logged-in-user/get-team-members-your-penpot.json",
"get-profiles-for-file-comments?file-id=*":
"workspace/get-profile-for-file-comments.json",
"update-profile-props": "workspace/update-profile-empty.json",
});
await BaseWebSocketPage.mockRPC(
page,
"get-profile",
"logged-in-user/get-profile-logged-in.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-team-users?file-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-comment-threads?file-id=*",
"workspace/get-comment-threads-empty.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-project?id=*",
"workspace/get-project-default.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-team?id=*",
"workspace/get-team-default.json",
);
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams.json");
await BaseWebSocketPage.mockRPC(
page,
"get-team-members?team-id=*",
"logged-in-user/get-team-members-your-penpot.json",
);
await BaseWebSocketPage.mockRPC(
page,
"get-profiles-for-file-comments?file-id=*",
"workspace/get-profile-for-file-comments.json",
);
await BaseWebSocketPage.mockRPC(
page,
"update-profile-props",
"workspace/update-profile-empty.json",
);
}
static anyTeamId = "c7ce0794-0992-8105-8004-38e630f7920a";
@@ -172,20 +62,9 @@ export class WorkspacePage extends BaseWebSocketPage {
static anyFileId = "c7ce0794-0992-8105-8004-38f280443849";
static anyPageId = "c7ce0794-0992-8105-8004-38f28044384a";
/**
* WebSocket mock
*
* @type {MockWebSocketHelper}
*/
#ws = null;
/**
* Constructor
*
* @param {Page} page
* @param {} [options]
*/
constructor(page, options) {
constructor(page) {
super(page);
this.pageName = page.getByTestId("page-name");
@@ -233,14 +112,11 @@ export class WorkspacePage extends BaseWebSocketPage {
"tokens-context-menu-for-set",
);
this.contextMenuForShape = page.getByTestId("context-menu");
if (options?.textEditor) {
this.textEditor = new WorkspacePage.TextEditor(this);
}
}
async goToWorkspace({
fileId = this.fileId ?? WorkspacePage.anyFileId,
pageId = this.pageId ?? WorkspacePage.anyPageId,
fileId = WorkspacePage.anyFileId,
pageId = WorkspacePage.anyPageId,
} = {}) {
await this.page.goto(
`/#/workspace?team-id=${WorkspacePage.anyTeamId}&file-id=${fileId}&page-id=${pageId}`,
@@ -265,59 +141,48 @@ export class WorkspacePage extends BaseWebSocketPage {
}
async setupEmptyFile() {
await this.mockRPCs({
"get-profile": "logged-in-user/get-profile-logged-in.json",
"get-team-users?file-id=*":
"logged-in-user/get-team-users-single-user.json ",
"get-comment-threads?file-id=*":
"workspace/get-comment-threads-empty.json",
"get-project?id=*": "workspace/get-project-default.json",
"get-team?id=*": "workspace/get-team-default.json",
"get-profiles-for-file-comments?file-id=*":
"workspace/get-profile-for-file-comments.json",
"get-file-object-thumbnails?file-id=*":
"workspace/get-file-object-thumbnails-blank.json",
"get-font-variants?team-id=*": "workspace/get-font-variants-empty.json",
"get-file-fragment?file-id=*": "workspace/get-file-fragment-blank.json",
"get-file-libraries?file-id=*": "workspace/get-file-libraries-empty.json",
});
if (this.textEditor) {
await this.mockRPC("update-file?id=*", "text-editor/update-file.json");
}
// by default we mock the blank file.
await this.mockGetFile("workspace/get-file-blank.json");
await this.mockRPC(
"get-profile",
"logged-in-user/get-profile-logged-in.json",
);
await this.mockRPC(
"get-team-users?file-id=*",
"logged-in-user/get-team-users-single-user.json",
);
await this.mockRPC(
"get-comment-threads?file-id=*",
"workspace/get-comment-threads-empty.json",
);
await this.mockRPC(
"get-project?id=*",
"workspace/get-project-default.json",
);
await this.mockRPC("get-team?id=*", "workspace/get-team-default.json");
await this.mockRPC(
"get-profiles-for-file-comments?file-id=*",
"workspace/get-profile-for-file-comments.json",
);
await this.mockRPC(/get\-file\?/, "workspace/get-file-blank.json");
await this.mockRPC(
"get-file-object-thumbnails?file-id=*",
"workspace/get-file-object-thumbnails-blank.json",
);
await this.mockRPC(
"get-font-variants?team-id=*",
"workspace/get-font-variants-empty.json",
);
await this.mockRPC(
"get-file-fragment?file-id=*",
"workspace/get-file-fragment-blank.json",
);
await this.mockRPC(
"get-file-libraries?file-id=*",
"workspace/get-file-libraries-empty.json",
);
}
async mockGetFile(jsonFilename, options) {
const page = this.page;
const jsonPath = `playwright/data/${jsonFilename}`;
const body = await readFile(jsonPath, "utf-8");
const payload = JSON.parse(body);
const fileId = Transit.get(payload, "id");
const pageId = Transit.get(payload, "data", "pages", 0);
const teamId = Transit.get(payload, "team-id");
this.fileId = fileId ?? this.anyFileId;
this.pageId = pageId ?? this.anyPageId;
this.teamId = teamId ?? this.anyTeamId;
const path = /get\-file\?/;
const url = typeof path === "string" ? `**/api/main/methods/${path}` : path;
const interceptConfig = {
status: 200,
contentType: "application/transit+json",
...options,
};
return page.route(url, (route) =>
route.fulfill({
...interceptConfig,
body,
}),
);
// await this.mockRPC(/get\-file\?/, jsonFile);
async mockGetFile(jsonFile) {
await this.mockRPC(/get\-file\?/, jsonFile);
}
async mockGetAsset(regex, asset) {
@@ -325,15 +190,22 @@ export class WorkspacePage extends BaseWebSocketPage {
}
async setupFileWithComments() {
await this.mockRPCs({
"get-comment-threads?file-id=*":
"workspace/get-comment-threads-unread.json",
"get-file-fragment?file-id=*&fragment-id=*":
"viewer/get-file-fragment-single-board.json",
"get-comments?thread-id=*": "workspace/get-thread-comments.json",
"update-comment-thread-status":
"workspace/update-comment-thread-status.json",
});
await this.mockRPC(
"get-comment-threads?file-id=*",
"workspace/get-comment-threads-unread.json",
);
await this.mockRPC(
"get-file-fragment?file-id=*&fragment-id=*",
"viewer/get-file-fragment-single-board.json",
);
await this.mockRPC(
"get-comments?thread-id=*",
"workspace/get-thread-comments.json",
);
await this.mockRPC(
"update-comment-thread-status",
"workspace/update-comment-thread-status.json",
);
}
async clickWithDragViewportAt(x, y, width, height) {
@@ -351,67 +223,6 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.mouse.up();
}
/**
* Clicks and moves from the coordinates x1,y1 to x2,y2
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
*/
async clickAndMove(x1, y1, x2, y2) {
await this.page.waitForTimeout(100);
await this.viewport.hover({ position: { x: x1, y: y1 } });
await this.page.mouse.down();
await this.viewport.hover({ position: { x: x2, y: y2 } });
await this.page.mouse.up();
}
/**
* Creates a new Text Shape in the specified coordinates
* with an initial text.
*
* @param {number} x1
* @param {number} y1
* @param {number} x2
* @param {number} y2
* @param {string} initialText
* @param {*} [options]
*/
async createTextShape(x1, y1, x2, y2, initialText, options) {
const timeToWait = options?.timeToWait ?? 100;
await this.page.keyboard.press("T");
await this.page.waitForTimeout(timeToWait);
await this.clickAndMove(x1, y1, x2, y2);
await this.page.waitForTimeout(timeToWait);
if (initialText) {
await this.page.keyboard.type(initialText);
}
}
/**
* Copies the selected element into the clipboard.
*
* @returns {Promise<void>}
*/
async copy() {
return this.page.keyboard.press("Control+C");
}
/**
* Pastes something from the clipboard.
*
* @param {"keyboard"|"context-menu"} [kind="keyboard"]
* @returns {Promise<void>}
*/
async paste(kind = "keyboard") {
if (kind === "context-menu") {
await this.viewport.click({ button: "right" });
return this.page.getByText("PasteCtrlV").click();
}
return this.page.keyboard.press("Control+V");
}
async panOnViewportAt(x, y, width, height) {
await this.page.waitForTimeout(100);
await this.viewport.hover({ position: { x, y } });
@@ -439,15 +250,10 @@ export class WorkspacePage extends BaseWebSocketPage {
await this.page.waitForTimeout(500);
}
async doubleClickLeafLayer(name, clickOptions = {}) {
await this.clickLeafLayer(name, clickOptions);
await this.clickLeafLayer(name, clickOptions);
}
async clickToggableLayer(name, clickOptions = {}) {
const layer = this.layers
.getByTestId("layer-row")
.filter({ hasText: name });
.getByTestId("layer-row")
.filter({ hasText: name });
const button = layer.getByRole("button");
await button.waitFor();

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View File

@@ -360,7 +360,7 @@ test("Renders a file with texts with paragraphs and breaking lines", async ({
id: "a5f238bd-dd8a-8164-8007-1bc3481eaf05",
pageId: "a5f238bd-dd8a-8164-8007-1bc3481eaf06",
});
await workspace.waitForFirstRenderWithoutUI();
await workspace.waitForFirstRender();
await expect(workspace.canvas).toHaveScreenshot();
});

View File

@@ -18,10 +18,6 @@ const setupFile = async (workspacePage) => {
fileId: "7b2da435-6186-815a-8007-0daa95d2f26d",
pageId: "ce79274b-11ab-8088-8007-0487ad43f789",
});
await workspacePage.mockRPC(
"update-file?id=*",
"workspace/update-file-empty.json",
);
};
const shapeToLayerName = {

View File

@@ -1,317 +1,12 @@
import { test, expect } from "@playwright/test";
import { Clipboard } from '../../helpers/Clipboard';
import { WorkspacePage } from "../pages/WorkspacePage";
const timeToWait = 100;
test.beforeEach(async ({ page, context }) => {
await Clipboard.enable(context, Clipboard.Permission.ONLY_WRITE);
test.beforeEach(async ({ page }) => {
await WorkspacePage.init(page);
await WorkspacePage.mockConfigFlags(page, ["enable-feature-text-editor-v2"]);
});
test.afterEach(async ({ context}) => {
context.clearPermissions();
})
test("Create a new text shape", async ({ page }) => {
const initialText = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await workspace.createTextShape(190, 150, 300, 200, initialText);
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(initialText);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text", async ({ page, context }) => {
const textToPaste = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockRPC(
"update-file?id=*",
"text-editor/update-file.json",
);
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("keyboard");
await page.waitForTimeout(timeToWait);
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
});
test("Create a new text shape from pasting text using context menu", async ({ page, context }) => {
const textToPaste = "Lorem ipsum";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickAt(190, 150);
await workspace.paste("context-menu");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe(textToPaste);
await workspace.textEditor.stopEditing();
})
test("Update an already created text shape by appending text", async ({ page }) => {
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd(0);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by prepending text", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(0);
await page.keyboard.type("Dolor sit amet ");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test("Update an already created text shape by inserting text in between", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart(5);
await page.keyboard.type(" dolor sit amet");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape appending text by pasting text", async ({ page, context }) => {
const textToPaste = " dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromEnd();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape prepending text by pasting text", async ({
page, context
}) => {
const textToPaste = "Dolor sit amet ";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await Clipboard.writeText(page, textToPaste);
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.moveFromStart();
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (starting) text with pasted text", async ({
page,
}) => {
const textToPaste = "Dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Dolor sit amet ipsum");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (ending) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromEnd(5);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem dolor sit amet");
await workspace.textEditor.stopEditing();
});
test("Update a new text shape replacing (in between) text with pasted text", async ({
page,
}) => {
const textToPaste = "dolor sit amet";
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5, 3);
await Clipboard.writeText(page, textToPaste);
await workspace.paste("keyboard");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lordolor sit ametsum");
await workspace.textEditor.stopEditing();
});
test("Update text font size selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC(
"update-file?id=*",
"text-editor/update-file.json",
);
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeFontSize(36);
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test.skip("Update text line height selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLineHeight(1.4);
const lineHeight = await workspace.textEditor.waitForParagraphStyle(1, 'line-height');
expect(lineHeight).toBe("1.4");
const textContent = await workspace.textEditor.waitForTextSpanContent();
expect(textContent).toBe("Lorem ipsum");
await workspace.textEditor.stopEditing();
});
test.skip("Update text letter spacing selecting a part of it (starting)", async ({
page,
}) => {
const workspace = new WorkspacePage(page, {
textEditor: true,
});
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-lorem-ipsum.json");
await workspace.mockRPC("update-file?id=*", "text-editor/update-file.json");
await workspace.goToWorkspace();
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.textEditor.startEditing();
await workspace.textEditor.selectFromStart(5);
await workspace.textEditor.changeLetterSpacing(10);
const textContent1 = await workspace.textEditor.waitForTextSpanContent(1);
expect(textContent1).toBe("Lorem");
const textContent2 = await workspace.textEditor.waitForTextSpanContent(2);
expect(textContent2).toBe(" ipsum");
await workspace.textEditor.stopEditing();
});
test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
test.skip("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
const workspace = new WorkspacePage(page);
await workspace.setupEmptyFile();
await workspace.mockGetFile("text-editor/get-file-11552.json");
@@ -319,16 +14,21 @@ test("BUG 11552 - Apply styles to the current caret", async ({ page }) => {
"update-file?id=*",
"text-editor/update-file-11552.json",
);
await workspace.goToWorkspace();
await workspace.doubleClickLeafLayer("Lorem ipsum");
await workspace.goToWorkspace({
fileId: "238a17e0-75ff-8075-8006-934586ea2230",
pageId: "238a17e0-75ff-8075-8006-934586ea2231",
});
await workspace.clickLeafLayer("Lorem ipsum");
await workspace.clickLeafLayer("Lorem ipsum");
const fontSizeInput = workspace.rightSidebar.getByRole("textbox", {
name: "Font Size",
});
await expect(fontSizeInput).toBeVisible();
await page.keyboard.press("Enter");
await page.keyboard.press("ArrowRight");
await workspace.page.keyboard.press("Enter");
await workspace.page.keyboard.press("ArrowRight");
await fontSizeInput.fill("36");

View File

@@ -180,8 +180,8 @@ export async function watch(baseDir, predicate, callback) {
});
}
async function readManifestFile() {
const manifestPath = "resources/public/js/manifest.json";
async function readManifestFile(resource) {
const manifestPath = "resources/public/" + resource;
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
return JSON.parse(content);
}
@@ -189,19 +189,23 @@ async function readManifestFile() {
async function readShadowManifest() {
const ts = Date.now();
try {
const content = await readManifestFile();
const content = await readManifestFile("js/manifest.json");
const index = {
ts: ts,
config: "js/config.js?ts=" + ts,
polyfills: "js/polyfills.js?ts=" + ts,
worker_main: "js/worker/main.js?ts=" + ts,
};
for (let item of content) {
index[item.name] = "js/" + item["output-name"];
}
const content2 = await readManifestFile("js/worker/manifest.json");
for (let item of content2) {
index["worker_" + item.name] = "js/worker/" + item["output-name"];
}
return index;
} catch (cause) {
return {

View File

@@ -20,16 +20,18 @@ echo $PATH
set -ex
corepack enable;
corepack install || exit 1;
corepack install;
yarn install || exit 1;
rm -rf resources/public;
rm -rf target/dist;
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS || exit 1
yarn run build:app:main --config-merge "{:release-version \"${CURRENT_HASH}-${TS}\"}" $EXTRA_PARAMS;
if [ "$INCLUDE_WASM" = "yes" ]; then
yarn run build:wasm || exit 1;
pushd ../render-wasm;
./build
popd
fi
yarn run build:app:libs || exit 1;
@@ -44,10 +46,6 @@ sed -i -re "s/\%version\%/$CURRENT_VERSION/g" ./target/dist/rasterizer.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/index.html;
sed -i -re "s/\%buildDate\%/$BUILD_DATE/g" ./target/dist/rasterizer.html;
if [ "$INCLUDE_WASM" = "yes" ]; then
sed -i "s/version=develop/version=$CURRENT_VERSION/g" ./target/dist/js/render_wasm.js;
fi
if [ "$INCLUDE_STORYBOOK" = "yes" ]; then
# build storybook
yarn run build:storybook || exit 1;

View File

@@ -83,7 +83,7 @@
:source-map-detail-level :all}}}
:worker
{:target :esm
{:target :browser
:output-dir "resources/public/js/worker/"
:asset-path "/js/worker"
:devtools {:browser-inject :main
@@ -94,6 +94,7 @@
{:main
{:entries [app.worker]
:web-worker true
:prepend-js "importScripts('/js/worker/render.js');"
:depends-on #{}}}
:js-options

View File

@@ -127,7 +127,7 @@
public-uri))
(def worker-uri
(obj/get global "penpotWorkerURI" "/js/worker.js"))
(obj/get global "penpotWorkerURI" "/js/worker/main.js"))
(defn external-feature-flag
[flag value]
@@ -189,7 +189,11 @@
(true? thumbnail?) (u/join (dm/str id "/thumbnail"))
(false? thumbnail?) (u/join (dm/str id)))))))
(defn resolve-static-asset
[path]
(let [uri (u/join public-uri path)]
(assoc uri :query (dm/str "version=" (:full version)))))
(defn resolve-href
[resource]
(let [version (get version :full)
href (-> public-uri
(u/ensure-path-slash)
(u/join resource)
(get :path))]
(str href "?version=" version)))

View File

@@ -76,7 +76,7 @@
(map :page-id))
(defn- apply-changes-localy
[{:keys [file-id redo-changes ignore-wasm?] :as commit} pending]
[{:keys [file-id redo-changes] :as commit} pending]
(ptk/reify ::apply-changes-localy
ptk/UpdateEvent
(update [_ state]
@@ -103,7 +103,7 @@
pids (into #{} xf:map-page-id redo-changes)]
(reduce #(ctst/update-object-indices %1 %2) fdata pids)))]
(if (and (not ignore-wasm?) (features/active-feature? state "render-wasm/v1"))
(if (features/active-feature? state "render-wasm/v1")
;; Update the wasm model
(let [shape-changes (volatile! {})
@@ -122,7 +122,7 @@
(defn commit
"Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn file-vern undo-group tags stack-undo? source ignore-wasm?]}]
file-id file-revn file-vern undo-group tags stack-undo? source]}]
(assert (cpc/check-changes redo-changes)
"expect valid vector of changes for redo-changes")
@@ -147,8 +147,7 @@
:save-undo? save-undo?
:undo-group undo-group
:tags tags
:stack-undo? stack-undo?
:ignore-wasm? ignore-wasm?}]
:stack-undo? stack-undo?}]
(ptk/reify ::commit
cljs.core/IDeref

View File

@@ -255,14 +255,19 @@
(defn- parse-sd-token-font-family-value
[value]
(let [missing-references (seq (some cto/find-token-value-references value))]
(let [value (-> (js->clj value) (flatten))
valid-font-family (or (string? value) (every? string? value))
missing-references (seq (some cto/find-token-value-references value))]
(cond
(not valid-font-family)
{:errors [(wte/error-with-value :error.style-dictionary/invalid-token-value-font-family value)]}
missing-references
{:errors [(wte/error-with-value :error.style-dictionary/missing-reference missing-references)]
:references missing-references}
:else
{:value (-> (js->clj value) (flatten))})))
{:value value})))
(defn parse-atomic-typography-value [token-type token-value]
(case token-type

View File

@@ -351,19 +351,31 @@
(on-success))))
(rx/catch on-error))))))
(def ^:private schema:create-invitation
[:and
[:map
[:emails {:optional true} [::sm/set ::sm/email]]
[:invitations {:optional true}
[:vector
[:map
[:email ::sm/email]
[:role [::sm/one-of ctt/valid-roles]]]]]
[:team-id ::sm/uuid]
[:resend? {:optional true} ::sm/boolean]]
[:fn (fn [attrs]
(or (contains? attrs :emails)
(contains? attrs :invitations)))]])
(def ^:private check-create-invitations-params
(sm/check-fn schema:create-invitation))
(defn create-invitations
"Unified function to create invitations. Supports two parameter formats:
1. {:emails #{...} :role :admin :team-id uuid} - single role for all emails
2. {:invitations [{:email ... :role ...}] :team-id uuid} - individual roles per email"
[{:keys [emails role team-id invitations resend?] :as params}]
(assert (uuid? team-id))
;; Validate input format - must have either emails+role OR invitations
(assert (or (and emails role (sm/check-set-of-emails emails) (keyword? role))
(and invitations
(sm/check-set-of-emails (map :email invitations))
(every? #(contains? ctt/valid-roles (:role %)) invitations)))
"Must provide either emails+role or invitations with individual roles")
(check-create-invitations-params params)
(ptk/reify ::create-invitations
ev/Event

View File

@@ -379,23 +379,6 @@
(->> (rx/from added)
(rx/map process-wasm-object)))))))
(when render-wasm?
(->> stream
(rx/filter (ptk/type? :wasm/position-data))
(rx/map deref)
(rx/filter
(fn [{:keys [position-data]}]
(some? position-data)))
(rx/map
(fn [{:keys [id position-data]}]
(prn "???" id position-data)
(dwsh/update-shapes
[id]
(fn [shape]
(.log js/console (clj->js shape))
(assoc shape :position-data position-data))
{:ignore-wasm? true})))))
(->> stream
(rx/filter dch/commit?)
(rx/map deref)

View File

@@ -102,8 +102,7 @@
{:origin it
:redo-changes changes
:undo-changes []
:save-undo? false
:ignore-wasm? true})))))))
:save-undo? false})))))))
;; FIXME: would be nice to not execute this code twice per page in the
;; same working session, maybe some local memoization can improve that
@@ -120,5 +119,4 @@
{:origin it
:redo-changes changes
:undo-changes []
:save-undo? false
:ignore-wasm? true})))))))
:save-undo? false})))))))

View File

@@ -649,7 +649,7 @@
(propagate-structure-modifiers modif-tree (dsh/lookup-page-objects state))
ids
(into (set (keys modif-tree)) xf:without-uuid-zero (keys transforms))
(into [] xf:without-uuid-zero (keys transforms))
update-shape
(fn [shape]

View File

@@ -50,8 +50,7 @@
([ids update-fn] (update-shapes ids update-fn nil))
([ids update-fn
{:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
ignore-touched undo-group with-objects? changed-sub-attr
ignore-wasm?]
ignore-touched undo-group with-objects? changed-sub-attr]
:or {reg-objects? false
save-undo? true
stack-undo? false
@@ -90,7 +89,6 @@
:ignore-tree ignore-tree
:ignore-touched ignore-touched
:with-objects? with-objects?})
(assoc :ignore-wasm? ignore-wasm?)
(cond-> undo-group
(pcb/set-undo-group undo-group)))

View File

@@ -831,8 +831,7 @@
(effect [_ state _]
(when (features/active-feature? state "text-editor/v2")
(let [instance (:workspace-editor state)
attrs-to-override (some-> (editor.v2/getCurrentStyle instance)
(styles/get-styles-from-style-declaration))
attrs-to-override (some-> (editor.v2/getCurrentStyle instance) (styles/get-styles-from-style-declaration))
overriden-attrs (merge attrs-to-override attrs)
styles (styles/attrs->styles overriden-attrs)]
(editor.v2/applyStylesToSelection instance styles))))))

View File

@@ -88,6 +88,10 @@
{:error/code :error.style-dictionary/invalid-token-value-font-weight
:error/fn #(tr "workspace.tokens.invalid-font-weight-token-value" %)}
:error.style-dictionary/invalid-token-value-font-family
{:error/code :error.style-dictionary/invalid-token-value-font-family
:error/fn #(tr "workspace.tokens.invalid-font-family-token-value" %)}
:error.style-dictionary/invalid-token-value-typography
{:error/code :error.style-dictionary/invalid-token-value-typography
:error/fn #(tr "workspace.tokens.invalid-token-value-typography" %)}

View File

@@ -26,7 +26,7 @@
(log/set-level! :warn)
(def google-fonts
(preload-gfonts "fonts/gfonts.2025.11.28.json"))
(preload-gfonts "fonts/gfonts.2025.05.19.json"))
(def local-fonts
[{:id "sourcesanspro"
@@ -342,8 +342,8 @@
(fn [result {:keys [font-id] :as node}]
(let [current-font
(if (some? font-id)
(select-keys node [:font-id :font-variant-id :font-weight :font-style])
(select-keys txt/default-typography [:font-id :font-variant-id :font-weight :font-style]))]
(select-keys node [:font-id :font-variant-id])
(select-keys txt/default-typography [:font-id :font-variant-id]))]
(conj result current-font)))
#{})))

View File

@@ -372,9 +372,6 @@
(def workspace-modifiers
(l/derived :workspace-modifiers st/state))
(def workspace-wasm-modifiers
(l/derived :workspace-wasm-modifiers st/state))
(def ^:private workspace-modifiers-with-objects
(l/derived
(fn [state]

View File

@@ -30,7 +30,6 @@
(def current-zoom (mf/create-context nil))
(def workspace-read-only? (mf/create-context nil))
(def is-render? (mf/create-context false))
(def is-component? (mf/create-context false))
(def sidebar

View File

@@ -45,7 +45,7 @@
.element-list {
@include t.use-typography("body-large");
color: var(--modal-text-foreground-color);
overflow-y: scroll;
overflow-y: auto;
margin-block: 0;
}

View File

@@ -223,24 +223,30 @@
circ (* 2 Math/PI 12)
pct (- circ (* circ (/ progress total)))
pwidth (if error?
280
(/ (* progress 280) total))
color (cond
error? clr/new-danger
healthy? (if is-default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
pwidth
(if error?
280
(/ (* progress 280) total))
background-clr (if is-default-theme?
clr/background-quaternary
clr/background-quaternary-light)
title (cond
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
color
(cond
error? clr/new-danger
healthy? (if is-default-theme?
clr/new-primary
clr/new-primary-light)
(not healthy?) clr/new-warning)
background-clr
(if is-default-theme?
clr/background-quaternary
clr/background-quaternary-light)
title
(cond
error? (tr "workspace.options.exporting-object-error")
complete? (tr "workspace.options.exporting-complete")
healthy? (tr "workspace.options.exporting-object")
(not healthy?) (tr "workspace.options.exporting-object-slow"))
retry-last-export
(mf/use-fn #(st/emit! (de/retry-last-export)))
@@ -284,7 +290,7 @@
:on-click retry-last-export}
(tr "workspace.options.retry")]
[:p {:class (stl/css :progress)}
[:span {:class (stl/css :progress)}
(dm/str progress " / " total)])]
[:button {:class (stl/css :progress-close-button)

View File

@@ -7,7 +7,6 @@
(ns app.main.ui.flex-controls.gap
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.geom.point :as gpt]
[app.common.geom.shapes :as gsh]
@@ -17,8 +16,6 @@
[app.common.types.shape.layout :as ctl]
[app.main.data.helpers :as dsh]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.css-cursors :as cur]
@@ -30,11 +27,10 @@
(mf/defc gap-display
[{:keys [frame-id zoom gap-type gap on-pointer-enter on-pointer-leave
rect-data hover? selected? mouse-pos hover-value
on-move-selected on-context-menu on-change]}]
on-move-selected on-context-menu]}]
(let [resizing (mf/use-var nil)
start (mf/use-var nil)
original-value (mf/use-var 0)
last-pos (mf/use-var nil)
negate? (:resize-negate? rect-data)
axis (:resize-axis rect-data)
@@ -47,55 +43,32 @@
(reset! start (dom/get-client-position event))
(reset! original-value (:initial-value rect-data))))
calc-modifiers
(mf/use-fn
(mf/deps frame-id gap-type gap)
(fn [pos]
(let [delta
(-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val
(int (max (+ @original-value (/ delta zoom)) 0))
layout-gap (assoc gap gap-type val)]
[val
(dwm/create-modif-tree
[frame-id]
(ctm/change-property (ctm/empty) :layout-gap layout-gap))])))
on-lost-pointer-capture
(mf/use-fn
(mf/deps calc-modifiers)
(mf/deps frame-id gap-type gap)
(fn [event]
(dom/release-pointer event)
(when (and (features/active-feature? @st/state "render-wasm/v1") (= @resizing gap-type))
(let [[_ modifiers] (calc-modifiers @last-pos)]
(st/emit! (dwm/apply-wasm-modifiers modifiers)
(dwt/finish-transform))))
(reset! resizing nil)
(reset! start nil)
(reset! original-value 0)
(when (not (features/active-feature? @st/state "render-wasm/v1"))
(st/emit! (dwm/apply-modifiers)))))
(st/emit! (dwm/apply-modifiers))))
on-pointer-move
(mf/use-fn
(mf/deps calc-modifiers on-change)
(mf/deps frame-id gap-type gap)
(fn [event]
(let [pos (dom/get-client-position event)]
(reset! last-pos pos)
(reset! mouse-pos (point->viewport pos))
(when (= @resizing gap-type)
(let [[val modifiers] (calc-modifiers pos)]
(let [delta (-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val (int (max (+ @original-value (/ delta zoom)) 0))
layout-gap (assoc gap gap-type val)
modifiers (dwm/create-modif-tree [frame-id] (ctm/change-property (ctm/empty) :layout-gap layout-gap))]
(reset! hover-value val)
(if (features/active-feature? @st/state "render-wasm/v1")
(st/emit! (dwm/set-wasm-modifiers modifiers))
(st/emit! (dwm/set-modifiers modifiers)))
(when on-change
(on-change modifiers)))))))]
(st/emit! (dwm/set-modifiers modifiers)))))))]
[:g.gap-rect
[:rect.info-area
@@ -147,17 +120,10 @@
pill-width (/ fcc/flex-display-pill-width zoom)
pill-height (/ fcc/flex-display-pill-height zoom)
workspace-modifiers (mf/deref refs/workspace-modifiers)
workspace-wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
gap-selected (mf/deref refs/workspace-gap-selected)
hover (mf/use-state nil)
hover-value (mf/use-state 0)
mouse-pos (mf/use-state nil)
current-modifiers (mf/use-state nil)
frame
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
padding (:layout-padding frame)
gap (:layout-gap frame)
{:keys [width height x1 y1]} (:selrect frame)
@@ -166,12 +132,6 @@
(reset! hover-value val))
on-pointer-leave #(reset! hover nil)
on-change
(mf/use-fn
(fn [modifiers]
(reset! current-modifiers modifiers)))
negate {:column-gap (if flip-x true false)
:row-gap (if flip-y true false)}
@@ -183,16 +143,8 @@
(= :column-reverse saved-dir))
(drop-last children)
(rest children))
children-to-display
(if (features/active-feature? @st/state "render-wasm/v1")
(let [modifiers (into {} workspace-wasm-modifiers)]
(->> children-to-display
;;(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))
(map (fn [shape]
(gsh/apply-transform shape (get modifiers (:id shape)))))))
(->> children-to-display
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers])))))
children-to-display (->> children-to-display
(map #(gsh/transform-shape % (get-in workspace-modifiers [(:id %) :modifiers]))))
wrap-blocks
(let [block-children (->> children
@@ -320,22 +272,20 @@
[:g.gaps {:pointer-events "visible"}
(for [[index display-item] (d/enumerate (concat display-blocks display-children))]
(let [gap-type (:gap-type display-item)]
[:& gap-display
{:key (str frame-id index)
:frame-id frame-id
:zoom zoom
:gap-type gap-type
:gap gap
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
:on-pointer-leave on-pointer-leave
:on-move-selected on-move-selected
:on-context-menu on-context-menu
:on-change on-change
:rect-data display-item
:hover? (= @hover gap-type)
:selected? (= gap-selected gap-type)
:mouse-pos mouse-pos
:hover-value hover-value}]))
[:& gap-display {:key (str frame-id index)
:frame-id frame-id
:zoom zoom
:gap-type gap-type
:gap gap
:on-pointer-enter (partial on-pointer-enter gap-type (get gap gap-type))
:on-pointer-leave on-pointer-leave
:on-move-selected on-move-selected
:on-context-menu on-context-menu
:rect-data display-item
:hover? (= @hover gap-type)
:selected? (= gap-selected gap-type)
:mouse-pos mouse-pos
:hover-value hover-value}]))
(when @hover
[:& fcc/flex-display-pill

View File

@@ -6,12 +6,9 @@
(ns app.main.ui.flex-controls.margin
(:require
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.types.modifiers :as ctm]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.css-cursors :as cur]
@@ -20,14 +17,11 @@
[app.util.dom :as dom]
[rumext.v2 :as mf]))
(mf/defc margin-display
[{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin
on-pointer-enter on-pointer-leave on-change
rect-data hover? selected? mouse-pos hover-value]}]
(mf/defc margin-display [{:keys [shape-id zoom hover-all? hover-v? hover-h? margin-num margin on-pointer-enter on-pointer-leave
rect-data hover? selected? mouse-pos hover-value]}]
(let [resizing? (mf/use-var false)
start (mf/use-var nil)
original-value (mf/use-var 0)
last-pos (mf/use-var nil)
negate? (true? (:resize-negate? rect-data))
axis (:resize-axis rect-data)
@@ -40,69 +34,39 @@
(reset! start (dom/get-client-position event))
(reset! original-value (:initial-value rect-data))))
calc-modifiers
(mf/use-fn
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
(fn [pos]
(let [delta
(-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val
(int (max (+ @original-value (/ delta zoom)) 0))
layout-item-margin
(cond
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
hover-v? (assoc margin :m1 val :m3 val)
hover-h? (assoc margin :m2 val :m4 val)
:else (assoc margin margin-num val))
layout-item-margin-type
(if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)]
[val
(dwm/create-modif-tree
[shape-id]
(-> (ctm/empty)
(ctm/change-property :layout-item-margin layout-item-margin)
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))])))
on-lost-pointer-capture
(mf/use-fn
(mf/deps calc-modifiers)
(mf/deps shape-id margin-num margin)
(fn [event]
(dom/release-pointer event)
(when (features/active-feature? @st/state "render-wasm/v1")
(let [[_ modifiers] (calc-modifiers @last-pos)]
(st/emit! (dwm/apply-wasm-modifiers modifiers)
(dwt/finish-transform))))
(reset! resizing? false)
(reset! start nil)
(reset! original-value 0)
(when (not (features/active-feature? @st/state "render-wasm/v1"))
(st/emit! (dwm/apply-modifiers)))))
(st/emit! (dwm/apply-modifiers))))
on-pointer-move
(mf/use-fn
(mf/deps calc-modifiers on-change)
(mf/deps shape-id margin-num margin hover-all? hover-v? hover-h?)
(fn [event]
(let [pos (dom/get-client-position event)]
(reset! mouse-pos (point->viewport pos))
(reset! last-pos pos)
(when @resizing?
(let [[val modifiers] (calc-modifiers pos)]
(let [delta (-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val (int (max (+ @original-value (/ delta zoom)) 0))
layout-item-margin (cond
hover-all? (assoc margin :m1 val :m2 val :m3 val :m4 val)
hover-v? (assoc margin :m1 val :m3 val)
hover-h? (assoc margin :m2 val :m4 val)
:else (assoc margin margin-num val))
layout-item-margin-type (if (= (:m1 margin) (:m2 margin) (:m3 margin) (:m4 margin)) :simple :multiple)
modifiers (dwm/create-modif-tree [shape-id]
(-> (ctm/empty)
(ctm/change-property :layout-item-margin layout-item-margin)
(ctm/change-property :layout-item-margin-type layout-item-margin-type)))]
(reset! hover-value val)
(if (features/active-feature? @st/state "render-wasm/v1")
(st/emit! (dwm/set-wasm-modifiers modifiers))
(st/emit! (dwm/set-modifiers modifiers)))
(when on-change
(on-change modifiers)))))))]
(st/emit! (dwm/set-modifiers modifiers)))))))]
[:rect.margin-rect
{:x (:x rect-data)
@@ -125,11 +89,6 @@
pill-width (/ fcc/flex-display-pill-width zoom)
pill-height (/ fcc/flex-display-pill-height zoom)
margins-selected (mf/deref refs/workspace-margins-selected)
current-modifiers (mf/use-state nil)
shape
(ctm/apply-structure-modifiers shape (dm/get-in @current-modifiers [shape-id :modifiers]))
hover-value (mf/use-state 0)
mouse-pos (mf/use-state nil)
hover (mf/use-state nil)
@@ -138,67 +97,50 @@
hover-h? (and (or (= @hover :m2) (= @hover :m4)) shift?)
margin (:layout-item-margin shape)
{:keys [width height x1 x2 y1 y2]} (:selrect shape)
on-pointer-enter
(mf/use-fn
(fn [hover-type val]
(reset! hover hover-type)
(reset! hover-value val)))
on-pointer-leave
(mf/use-fn
(fn []
(reset! hover nil)))
on-change
(mf/use-fn
(fn [modifiers]
(reset! current-modifiers modifiers)))
hover?
(fn [value]
(or hover-all?
(and (or (= value :m1) (= value :m3)) hover-v?)
(and (or (= value :m2) (= value :m4)) hover-h?)
(= @hover value)))
margin-display-data
{:m1 {:key (str shape-id "-m1")
:x x1
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
:width width
:height (:m1 margin)
:initial-value (:m1 margin)
:resize-type :top
:resize-axis :y
:resize-negate? (:flip-y frame)}
:m2 {:key (str shape-id "-m2")
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
:y y1
:width (:m2 margin)
:height height
:initial-value (:m2 margin)
:resize-type :left
:resize-axis :x
:resize-negate? (:flip-x frame)}
:m3 {:key (str shape-id "-m3")
:x x1
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
:width width
:height (:m3 margin)
:initial-value (:m3 margin)
:resize-type :top
:resize-axis :y
:resize-negate? (:flip-y frame)}
:m4 {:key (str shape-id "-m4")
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
:y y1
:width (:m4 margin)
:height height
:initial-value (:m4 margin)
:resize-type :left
:resize-axis :x
:resize-negate? (:flip-x frame)}}]
on-pointer-enter (fn [hover-type val]
(reset! hover hover-type)
(reset! hover-value val))
on-pointer-leave #(reset! hover nil)
hover? #(or hover-all?
(and (or (= % :m1) (= % :m3)) hover-v?)
(and (or (= % :m2) (= % :m4)) hover-h?)
(= @hover %))
margin-display-data {:m1 {:key (str shape-id "-m1")
:x x1
:y (if (:flip-y frame) y2 (- y1 (:m1 margin)))
:width width
:height (:m1 margin)
:initial-value (:m1 margin)
:resize-type :top
:resize-axis :y
:resize-negate? (:flip-y frame)}
:m2 {:key (str shape-id "-m2")
:x (if (:flip-x frame) (- x1 (:m2 margin)) x2)
:y y1
:width (:m2 margin)
:height height
:initial-value (:m2 margin)
:resize-type :left
:resize-axis :x
:resize-negate? (:flip-x frame)}
:m3 {:key (str shape-id "-m3")
:x x1
:y (if (:flip-y frame) (- y1 (:m3 margin)) y2)
:width width
:height (:m3 margin)
:initial-value (:m3 margin)
:resize-type :top
:resize-axis :y
:resize-negate? (:flip-y frame)}
:m4 {:key (str shape-id "-m4")
:x (if (:flip-x frame) x2 (- x1 (:m4 margin)))
:y y1
:width (:m4 margin)
:height height
:initial-value (:m4 margin)
:resize-type :left
:resize-axis :x
:resize-negate? (:flip-x frame)}}]
[:g.margins {:pointer-events "visible"}
(for [[margin-num rect-data] margin-display-data]
@@ -213,7 +155,6 @@
:margin margin
:on-pointer-enter (partial on-pointer-enter margin-num (get margin margin-num))
:on-pointer-leave on-pointer-leave
:on-change on-change
:rect-data rect-data
:hover? (hover? margin-num)
:selected? (get margins-selected margin-num)

View File

@@ -6,12 +6,9 @@
(ns app.main.ui.flex-controls.padding
(:require
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.common.types.modifiers :as ctm]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.css-cursors :as cur]
@@ -21,13 +18,11 @@
[rumext.v2 :as mf]))
(mf/defc padding-display
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter
on-pointer-leave rect-data hover? selected? mouse-pos hover-value on-move-selected
on-context-menu on-change]}]
[{:keys [frame-id zoom hover-all? hover-v? hover-h? padding-num padding on-pointer-enter on-pointer-leave
rect-data hover? selected? mouse-pos hover-value on-move-selected on-context-menu]}]
(let [resizing? (mf/use-var false)
start (mf/use-var nil)
original-value (mf/use-var 0)
last-pos (mf/use-var nil)
negate? (true? (:resize-negate? rect-data))
axis (:resize-axis rect-data)
@@ -40,69 +35,41 @@
(reset! start (dom/get-client-position event))
(reset! original-value (:initial-value rect-data))))
calc-modifiers
(mf/use-fn
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
(fn [pos]
(let [delta
(-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val
(int (max (+ @original-value (/ delta zoom)) 0))
layout-padding
(cond
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
hover-v? (assoc padding :p1 val :p3 val)
hover-h? (assoc padding :p2 val :p4 val)
:else (assoc padding padding-num val))
layout-padding-type
(if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)]
[val
(dwm/create-modif-tree
[frame-id]
(-> (ctm/empty)
(ctm/change-property :layout-padding layout-padding)
(ctm/change-property :layout-padding-type layout-padding-type)))])))
on-lost-pointer-capture
(mf/use-fn
(mf/deps calc-modifiers)
(mf/deps frame-id padding-num padding)
(fn [event]
(dom/release-pointer event)
(when (features/active-feature? @st/state "render-wasm/v1")
(let [[_ modifiers] (calc-modifiers @last-pos)]
(st/emit! (dwm/apply-wasm-modifiers modifiers)
(dwt/finish-transform))))
(reset! resizing? false)
(reset! start nil)
(reset! original-value 0)
(when (not (features/active-feature? @st/state "render-wasm/v1"))
(st/emit! (dwm/apply-modifiers)))))
(st/emit! (dwm/apply-modifiers))))
on-pointer-move
(mf/use-fn
(mf/deps calc-modifiers on-change)
(mf/deps frame-id padding-num padding hover-all? hover-v? hover-h?)
(fn [event]
(let [pos (dom/get-client-position event)]
(reset! mouse-pos (point->viewport pos))
(reset! last-pos pos)
(when @resizing?
(let [[val modifiers] (calc-modifiers pos)]
(reset! hover-value val)
(if (features/active-feature? @st/state "render-wasm/v1")
(st/emit! (dwm/set-wasm-modifiers modifiers))
(st/emit! (dwm/set-modifiers modifiers)))
(let [delta (-> (gpt/to-vec @start pos)
(cond-> negate? gpt/negate)
(get axis))
val (int (max (+ @original-value (/ delta zoom)) 0))
layout-padding (cond
hover-all? (assoc padding :p1 val :p2 val :p3 val :p4 val)
hover-v? (assoc padding :p1 val :p3 val)
hover-h? (assoc padding :p2 val :p4 val)
:else (assoc padding padding-num val))
(when on-change
(on-change modifiers)))))))]
layout-padding-type (if (= (:p1 padding) (:p2 padding) (:p3 padding) (:p4 padding)) :simple :multiple)
modifiers (dwm/create-modif-tree [frame-id]
(-> (ctm/empty)
(ctm/change-property :layout-padding layout-padding)
(ctm/change-property :layout-padding-type layout-padding-type)))]
(reset! hover-value val)
(st/emit! (dwm/set-modifiers modifiers)))))))]
[:g.padding-rect
[:rect.info-area
@@ -138,108 +105,77 @@
:on-lost-pointer-capture on-lost-pointer-capture
:on-pointer-move on-pointer-move
:on-context-menu on-context-menu
:class
(when (or hover? selected?)
(if (= (:resize-axis rect-data) :x)
(cur/get-dynamic "resize-ew" 0)
(cur/get-dynamic "resize-ew" 90)))
:style
{:fill (if (or hover? selected?) fcc/distance-color "none")
:opacity (if selected? 0 1)}}])]))
:class (when (or hover? selected?)
(if (= (:resize-axis rect-data) :x) (cur/get-dynamic "resize-ew" 0) (cur/get-dynamic "resize-ew" 90)))
:style {:fill (if (or hover? selected?) fcc/distance-color "none")
:opacity (if selected? 0 1)}}])]))
(mf/defc padding-rects
[{:keys [frame zoom alt? shift? on-move-selected on-context-menu]}]
(let [frame-id (:id frame)
paddings-selected (mf/deref refs/workspace-paddings-selected)
current-modifiers (mf/use-state nil)
frame
(ctm/apply-structure-modifiers frame (dm/get-in @current-modifiers [frame-id :modifiers]))
hover-value (mf/use-state 0)
mouse-pos (mf/use-state nil)
hover (mf/use-state nil)
hover-all? (and (not (nil? @hover)) alt?)
hover-v? (and (or (= @hover :p1) (= @hover :p3)) shift?)
hover-h? (and (or (= @hover :p2) (= @hover :p4)) shift?)
padding (:layout-padding frame)
{:keys [width height x1 x2 y1 y2]} (:selrect frame)
on-pointer-enter (fn [hover-type val]
(reset! hover hover-type)
(reset! hover-value val))
on-pointer-leave #(reset! hover nil)
pill-width (/ fcc/flex-display-pill-width zoom)
pill-height (/ fcc/flex-display-pill-height zoom)
hover? #(or hover-all?
(and (or (= % :p1) (= % :p3)) hover-v?)
(and (or (= % :p2) (= % :p4)) hover-h?)
(= @hover %))
negate {:p1 (if (:flip-y frame) true false)
:p2 (if (:flip-x frame) true false)
:p3 (if (:flip-y frame) true false)
:p4 (if (:flip-x frame) true false)}
negate (cond-> negate
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
negate
{:p1 (if (:flip-y frame) true false)
:p2 (if (:flip-x frame) true false)
:p3 (if (:flip-y frame) true false)
:p4 (if (:flip-x frame) true false)}
negate
(cond-> negate
(not= :auto (:layout-item-h-sizing frame)) (assoc :p2 (not (:p2 negate)))
(not= :auto (:layout-item-v-sizing frame)) (assoc :p3 (not (:p3 negate))))
padding-rect-data
{:p1 {:key (str frame-id "-p1")
:x x1
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
:width width
:height (:p1 padding)
:initial-value (:p1 padding)
:resize-type (if (:flip-y frame) :bottom :top)
:resize-axis :y
:resize-negate? (:p1 negate)}
:p2 {:key (str frame-id "-p2")
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
:y y1
:width (:p2 padding)
:height height
:initial-value (:p2 padding)
:resize-type :left
:resize-axis :x
:resize-negate? (:p2 negate)}
:p3 {:key (str frame-id "-p3")
:x x1
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
:width width
:height (:p3 padding)
:initial-value (:p3 padding)
:resize-type :bottom
:resize-axis :y
:resize-negate? (:p3 negate)}
:p4 {:key (str frame-id "-p4")
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
:y y1
:width (:p4 padding)
:height height
:initial-value (:p4 padding)
:resize-type (if (:flip-x frame) :right :left)
:resize-axis :x
:resize-negate? (:p4 negate)}}
on-pointer-enter
(mf/use-fn
(fn [hover-type val]
(reset! hover hover-type)
(reset! hover-value val)))
on-pointer-leave
(mf/use-fn
(fn []
(reset! hover nil)))
on-change
(mf/use-fn
(fn [modifiers]
(reset! current-modifiers modifiers)))
hover?
(fn [value]
(or hover-all?
(and (or (= value :p1) (= value :p3)) hover-v?)
(and (or (= value :p2) (= value :p4)) hover-h?)
(= @hover value)))]
padding-rect-data {:p1 {:key (str frame-id "-p1")
:x x1
:y (if (:flip-y frame) (- y2 (:p1 padding)) y1)
:width width
:height (:p1 padding)
:initial-value (:p1 padding)
:resize-type (if (:flip-y frame) :bottom :top)
:resize-axis :y
:resize-negate? (:p1 negate)}
:p2 {:key (str frame-id "-p2")
:x (if (:flip-x frame) x1 (- x2 (:p2 padding)))
:y y1
:width (:p2 padding)
:height height
:initial-value (:p2 padding)
:resize-type :left
:resize-axis :x
:resize-negate? (:p2 negate)}
:p3 {:key (str frame-id "-p3")
:x x1
:y (if (:flip-y frame) y1 (- y2 (:p3 padding)))
:width width
:height (:p3 padding)
:initial-value (:p3 padding)
:resize-type :bottom
:resize-axis :y
:resize-negate? (:p3 negate)}
:p4 {:key (str frame-id "-p4")
:x (if (:flip-x frame) (- x2 (:p4 padding)) x1)
:y y1
:width (:p4 padding)
:height height
:initial-value (:p4 padding)
:resize-type (if (:flip-x frame) :right :left)
:resize-axis :x
:resize-negate? (:p4 negate)}}]
[:g.paddings {:pointer-events "visible"}
(for [[padding-num rect-data] padding-rect-data]
@@ -258,11 +194,9 @@
:on-pointer-leave on-pointer-leave
:on-move-selected on-move-selected
:on-context-menu on-context-menu
:on-change on-change
:hover? (hover? padding-num)
:selected? (get paddings-selected padding-num)
:rect-data rect-data}])
(when @hover
[:& fcc/flex-display-pill
{:height pill-height

View File

@@ -36,7 +36,7 @@
:text [:visibility :geometry :text :shadow :blur :stroke :layout-element]
:variant [:variant :geometry :fill :stroke :shadow :blur :layout :layout-element]})
(mf/defc attributes
(mf/defc attributes*
[{:keys [page-id file-id shapes frame from libraries share-id objects color-space]}]
(let [shapes (hooks/use-equal-memo shapes)
first-shape (first shapes)

View File

@@ -96,7 +96,7 @@
embed-images? (replace-map images-data))]
(str/format page-template style-code markup-code)))
(mf/defc code
(mf/defc code*
[{:keys [shapes frame on-expand from]}]
(let [style-type* (mf/use-state "css")
markup-type* (mf/use-state "html")

View File

@@ -16,8 +16,8 @@
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.icons :as deprecated-icon]
[app.main.ui.inspect.attributes :refer [attributes]]
[app.main.ui.inspect.code :refer [code]]
[app.main.ui.inspect.attributes :refer [attributes*]]
[app.main.ui.inspect.code :refer [code*]]
[app.main.ui.inspect.selection-feedback :refer [resolve-shapes]]
[app.main.ui.inspect.styles :refer [styles-tab*]]
[app.util.dom :as dom]
@@ -122,8 +122,7 @@
(fn []
(if (seq shapes)
(st/emit! (ptk/event ::ev/event {::ev/name "inspect-mode-click-element"}))
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))
(reset! color-space* "hex")))
(handle-change-tab (if (contains? cf/flags :inspect-styles) :styles :info)))))
[:aside {:class (stl/css-case :settings-bar-right true
:viewer-code (= from :viewer))}
@@ -189,41 +188,41 @@
:libraries libraries
:file-id file-id}]
:computed
[:& attributes {:color-space color-space
:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
[:> attributes* {:color-space color-space
:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]
[:> code* {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])]
[:> tab-switcher* {:tabs tabs
:selected (name @section)
:on-change handle-change-tab
:class (stl/css :viewer-tab-switcher)}
(case @section
:info
[:& attributes {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
[:> attributes* {:page-id page-id
:objects objects
:file-id file-id
:frame frame
:shapes shapes
:from from
:libraries libraries
:share-id share-id}]
:code
[:& code {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:> code* {:frame frame
:shapes shapes
:on-expand handle-expand
:from from}])])]]
[:div {:class (stl/css :empty)}
[:div {:class (stl/css :code-info)}
[:span {:class (stl/css :placeholder-icon)}

View File

@@ -133,7 +133,7 @@
(swap! shorthands* assoc (:panel shorthand) (:property shorthand))))]
[:ol {:class (stl/css :styles-tab) :aria-label (tr "labels.styles")}
;; TOKENS PANEL
(when (or active-themes active-sets)
(when (or (seq active-themes) (seq active-sets))
[:li
[:> style-box* {:panel :token}
[:> tokens-panel* {:theme-paths active-themes :set-names active-sets}]]])

View File

@@ -77,7 +77,7 @@
[:button {:class (stl/css :cta-button :bottom-link)
:on-click cta-link-trial} cta-text-trial])])
(defn schema:seats-form [min-editors]
(defn- make-management-form-schema [min-editors]
[:map {:title "SeatsForm"}
[:min-members [::sm/number {:min min-editors
:max 9999}]]
@@ -87,7 +87,6 @@
{::mf/register modal/components
::mf/register-as :management-dialog}
[{:keys [subscription-type current-subscription editors subscribe-to-trial]}]
(let [unlimited-modal-step*
(mf/use-state 1)
@@ -112,9 +111,12 @@
{:min-members min-editors
:redirect-to-payment-details false})
schema
(mf/with-memo [min-editors]
(make-management-form-schema min-editors))
form
(fm/use-form :schema (schema:seats-form min-editors)
:initial initial)
(fm/use-form :schema schema :initial initial)
submit-in-progress
(mf/use-ref false)
@@ -334,11 +336,15 @@
[:> raw-svg* {:id (if (= "light" (:theme profile)) "logo-subscription-light" "logo-subscription")}]]
[:div {:class (stl/css :modal-end)}
[:div {:class (stl/css :modal-title)} (tr "subscription.settings.sucess.dialog.title" subscription-name)]
[:div {:class (stl/css :modal-title)}
(tr "subscription.settings.sucess.dialog.title" subscription-name)]
(when (not= subscription-name "professional")
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.thanks" subscription-name)])
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)} (tr "subscription.settings.sucess.dialog.footer")]
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.thanks" subscription-name)])
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.success.dialog.description")]
[:p {:class (stl/css :modal-text-large)}
(tr "subscription.settings.sucess.dialog.footer")]
[:div {:class (stl/css :success-action-buttons)}
[:input
@@ -418,7 +424,11 @@
(mf/with-effect []
(dom/set-html-title (tr "subscription.labels")))
(mf/with-effect [authenticated? show-subscription-success-modal? show-trial-subscription-modal? success-modal-is-trial? subscription]
(mf/with-effect [authenticated?
show-subscription-success-modal?
show-trial-subscription-modal?
success-modal-is-trial?
subscription]
(when ^boolean authenticated?
(cond
^boolean show-trial-subscription-modal?

View File

@@ -28,7 +28,6 @@
{::mf/wrap-props false}
[props]
(let [{:keys [position-data content] :as shape} (obj/get props "shape")
is-render? (mf/use-ctx ctx/is-render?)
is-component? (mf/use-ctx ctx/is-component?)]
(mf/with-memo [content]
@@ -42,5 +41,5 @@
;; Only use this for component preview, otherwise the dashboard thumbnails
;; will give a tainted canvas error because the `foreignObject` cannot be
;; rendered.
(and (nil? position-data) (or is-component? is-render?))
(and (nil? position-data) is-component?)
[:> fo/text-shape props])))

View File

@@ -27,7 +27,7 @@
[app.main.ui.workspace.coordinates :as coordinates]
[app.main.ui.workspace.libraries]
[app.main.ui.workspace.nudge]
[app.main.ui.workspace.palette :refer [palette]]
[app.main.ui.workspace.palette :refer [palette*]]
[app.main.ui.workspace.plugins]
[app.main.ui.workspace.sidebar :refer [sidebar*]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
@@ -84,8 +84,8 @@
node-ref (use-resize-observer on-resize)]
[:*
(when (not ^boolean hide-ui?)
[:& palette {:layout layout
:on-change-palette-size on-resize-palette}])
[:> palette* {:layout layout
:on-change-size on-resize-palette}])
[:section
{:key (dm/str "workspace-" page-id)

View File

@@ -156,7 +156,7 @@
(let [{:keys [modal title]} (get dwta/token-properties :color)
window-size (dom/get-window-size)
left-sidebar (dom/get-element "left-sidebar-aside")
x-size (dom/get-data left-sidebar "left-sidebar-width")
x-size (dom/get-data left-sidebar "width")
modal-height 392
x (- (int x-size) 30)
y (- (/ (:height window-size) 2) (/ modal-height 2))]

View File

@@ -33,12 +33,13 @@
[okulary.core :as l]
[rumext.v2 :as mf]))
(def viewport
(def ^:private ref:viewport
(l/derived :vport refs/workspace-local))
(defn calculate-palette-padding [rulers?]
(defn- calculate-palette-style
[rulers?]
(let [left-sidebar (dom/get-element "left-sidebar-aside")
left-sidebar-size (-> (dom/get-data left-sidebar "left-sidebar-width")
left-sidebar-size (-> (dom/get-data left-sidebar "width")
(d/parse-integer))
rulers-width (if rulers? 22 0)
min-left-sidebar-width left-sidebar-default-width
@@ -48,36 +49,46 @@
#js {"paddingLeft" (dm/str calculate-padding-left "px")
"paddingRight" "322px"}))
(mf/defc palette
[{:keys [layout on-change-palette-size]}]
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil)
state* (mf/use-state {:show-menu false})
state (deref state*)
show-menu? (:show-menu state)
selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent)
selected-text* (mf/use-state :file)
selected-text (deref selected-text*)
on-select (mf/use-fn #(reset! selected %))
rulers? (mf/deref refs/rulers?)
{:keys [on-pointer-down on-lost-pointer-capture on-pointer-move parent-ref size]}
(r/use-resize-hook :palette 72 54 80 :y true :bottom on-change-palette-size)
(mf/defc palette*
[{:keys [layout on-change-size]}]
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
vport (mf/deref viewport)
vport-width (:width vport)
read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil)
state* (mf/use-state #(-> {:show-menu false}))
state (deref state*)
show-menu? (:show-menu state)
selected (h/use-shared-state mdc/colorpalette-selected-broadcast-key :recent)
selected-text* (mf/use-state :file)
selected-text (deref selected-text*)
on-select (mf/use-fn #(reset! selected %))
rulers? (mf/deref refs/rulers?)
vport (mf/deref ref:viewport)
vport-width (get vport :width)
{:keys [on-pointer-down
on-lost-pointer-capture
on-pointer-move
parent-ref
size]}
(r/use-resize-hook :palette 72 54 80 :y true :bottom on-change-size)
on-resize
(mf/use-callback
(mf/use-fn
(fn [_]
(let [dom (mf/ref-val container)
width (obj/get dom "clientWidth")]
(swap! state* assoc :width width))))
on-close-menu
(mf/use-callback
(mf/use-fn
(fn [_]
(swap! state* assoc :show-menu false)))
@@ -100,7 +111,7 @@
(reset! selected-text* (:id lib)))))
toggle-palettes
(mf/use-callback
(mf/use-fn
(fn [_]
(r/set-resize-type! :top)
(dom/add-class! (dom/get-element-by-class "color-palette") "fade-out-down")
@@ -131,7 +142,9 @@
(vary-meta assoc ::ev/origin "workspace-left-toolbar"))))
(dom/blur! node))))
any-palette? (or color-palette? text-palette?)
any-palette?
(or color-palette? text-palette?)
size-classname
(cond
(<= size 64) (stl/css :small-palette)
@@ -142,16 +155,16 @@
(let [key1 (events/listen js/window "resize" on-resize)]
#(events/unlistenByKey key1)))
(mf/use-layout-effect
#(let [dom (mf/ref-val parent-ref)
(mf/with-layout-effect []
(let [dom (mf/ref-val parent-ref)
width (obj/get dom "clientWidth")]
(swap! state* assoc :width width)))
[:div {:class (stl/css :palette-wrapper)
:id "palette-wrapper"
:style (calculate-palette-padding rulers?)
:style (calculate-palette-style rulers?)
:data-testid "palette"}
(when-not workspace-read-only?
(when-not ^boolean read-only?
[:div {:ref parent-ref
:class (dm/str size-classname " " (stl/css-case :palettes true
:wide any-palette?

View File

@@ -12,20 +12,18 @@
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
[app.render-wasm.api :as wasm.api]
[rumext.v2 :as mf]))
(mf/defc text-edition-outline
[{:keys [shape zoom modifiers]}]
(if (features/active-feature? @st/state "render-wasm/v1")
(let [{:keys [width height]} (wasm.api/get-text-dimensions (:id shape))
selrect-transform (mf/deref refs/workspace-selrect)
[selrect transform] (dsh/get-selrect selrect-transform shape)]
(let [selrect-transform (mf/deref refs/workspace-selrect)
[{:keys [x y width height]} transform] (dsh/get-selrect selrect-transform shape)]
[:rect.main.viewport-selrect
{:x (:x selrect)
:y (:y selrect)
:width (max width (:width selrect))
:height (max height (:height selrect))
{:x x
:y y
:width width
:height height
:transform transform
:style {:stroke "var(--color-accent-tertiary)"
:stroke-width (/ 1 zoom)

View File

@@ -320,12 +320,10 @@
[{:keys [x y width height]} transform]
(if render-wasm?
(let [{:keys [width height]} (wasm.api/get-text-dimensions shape-id)
(let [{:keys [height]} (wasm.api/get-text-dimensions shape-id)
selrect-transform (mf/deref refs/workspace-selrect)
[selrect transform] (dsh/get-selrect selrect-transform shape)
selrect-height (:height selrect)
selrect-width (:width selrect)
max-width (max width selrect-width)
max-height (max height selrect-height)
valign (-> shape :content :vertical-align)
y (:y selrect)
@@ -335,7 +333,7 @@
"center" (- y (/ (- height selrect-height) 2))
"top" y)
y)]
[(assoc selrect :y y :width max-width :height max-height) transform])
[(assoc selrect :y y :width (:width selrect) :height max-height) transform])
(let [bounds (gst/shape->rect shape)
x (mth/min (dm/get-prop bounds :x)
@@ -354,7 +352,7 @@
(obj/merge!
#js {"--editor-container-width" (dm/str width "px")
"--editor-container-height" (dm/str height "px")
"--fallback-families" (if (seq fallback-families) (dm/str (str/join ", " fallback-families)) "sourcesanspro")})
"--fallback-families" (dm/str (str/join ", " fallback-families))})
(not render-wasm?)
(obj/merge!

View File

@@ -27,7 +27,6 @@
[app.main.ui.workspace.left-header :refer [left-header*]]
[app.main.ui.workspace.right-header :refer [right-header*]]
[app.main.ui.workspace.sidebar.assets :refer [assets-toolbox*]]
[app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button*]]
[app.main.ui.workspace.sidebar.debug :refer [debug-panel*]]
[app.main.ui.workspace.sidebar.debug-shape-info :refer [debug-shape-info*]]
[app.main.ui.workspace.sidebar.history :refer [history-toolbox*]]
@@ -44,19 +43,34 @@
;; --- Left Sidebar (Component)
(defn- on-collapse-left-sidebar
[]
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(def ^:private toggle-collapse-left-sidebar
(partial st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(mf/defc collapse-button*
{::mf/private true}
[]
;; NOTE: This custom button may be replace by an action button when this variant is designed
[:button {:class (stl/css :collapse-sidebar-button)
:on-click on-collapse-left-sidebar}
:on-click toggle-collapse-left-sidebar}
[:> icon* {:icon-id i/arrow
:size "s"
:aria-label (tr "workspace.sidebar.collapse")}]])
(mf/defc collapsed-button*
{::mf/memo true
::mf/private true}
[]
[:div {:id "left-sidebar-aside"
:data-width "0"
:class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button)
:title (tr "workspace.sidebar.expand")
:on-click toggle-collapse-left-sidebar}
[:> icon* {:icon-id i/arrow
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]])
(mf/defc layers-content*
{::mf/private true
::mf/memo true}
@@ -97,6 +111,7 @@
[:> layers-toolbox* {:size-parent width}]]))
(mf/defc left-sidebar*
{::mf/memo true}
[{:keys [layout file page-id tokens-lib active-tokens resolved-active-tokens]}]
@@ -161,7 +176,7 @@
[:aside {:ref parent-ref
:id "left-sidebar-aside"
:data-testid "left-sidebar"
:data-left-sidebar-width (str width)
:data-width (str width)
:class aside-class
:style {:--left-sidebar-width (dm/str width "px")}}

View File

@@ -116,6 +116,44 @@
}
}
.collapsed-sidebar {
@include deprecated.flexCenter;
position: absolute;
top: deprecated.$s-48;
left: 0;
padding: deprecated.$s-4;
border-radius: deprecated.$br-8;
background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
}
.collapsed-title {
@include deprecated.flexCenter;
height: deprecated.$s-36;
width: deprecated.$s-24;
border-radius: deprecated.$br-8;
background: var(--color-background-secondary);
}
.collapsed-button {
@include deprecated.buttonStyle;
height: deprecated.$s-24;
width: deprecated.$s-16;
padding: 0;
border-radius: deprecated.$br-5;
svg {
@include deprecated.flexCenter;
height: deprecated.$s-16;
width: deprecated.$s-16;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
&:hover {
svg {
stroke: var(--icon-foreground-hover);
}
}
}
.versions-tab {
width: 100%;
overflow: hidden;

View File

@@ -1,29 +0,0 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.sidebar.collapsable-button
(:require-macros [app.main.style :as stl])
(:require
[app.main.data.workspace :as dw]
[app.main.store :as st]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc collapsed-button*
{::mf/memo true}
[]
(let [on-click (mf/use-fn #(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))]
[:div {:id "left-sidebar-aside"
:data-size "0"
:class (stl/css :collapsed-sidebar)}
[:div {:class (stl/css :collapsed-title)}
[:button {:class (stl/css :collapsed-button)
:title (tr "workspace.sidebar.expand")
:on-click on-click}
[:> icon* {:icon-id i/arrow
:size "s"
:aria-label (tr "workspace.sidebar.expand")}]]]]))

View File

@@ -1,45 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated;
.collapsed-sidebar {
@include deprecated.flexCenter;
position: absolute;
top: deprecated.$s-48;
left: 0;
padding: deprecated.$s-4;
border-radius: deprecated.$br-8;
background: var(--color-background-primary);
margin-inline-start: var(--sp-m);
}
.collapsed-title {
@include deprecated.flexCenter;
height: deprecated.$s-36;
width: deprecated.$s-24;
border-radius: deprecated.$br-8;
background: var(--color-background-secondary);
}
.collapsed-button {
@include deprecated.buttonStyle;
height: deprecated.$s-24;
width: deprecated.$s-16;
padding: 0;
border-radius: deprecated.$br-5;
svg {
@include deprecated.flexCenter;
height: deprecated.$s-16;
width: deprecated.$s-16;
color: transparent;
fill: none;
stroke: var(--icon-foreground);
}
&:hover {
svg {
stroke: var(--icon-foreground-hover);
}
}
}

View File

@@ -378,7 +378,6 @@
:step 0.1
:default-value "1.2"
:class (stl/css :line-height-input)
:aria-label (tr "inspect.attributes.typography.line-height")
:value (attr->string line-height)
:placeholder (if (= :multiple line-height) (tr "settings.multiple") "--")
:nillable (= :multiple line-height)
@@ -397,7 +396,6 @@
:step 0.1
:default-value "0"
:class (stl/css :letter-spacing-input)
:aria-label (tr "inspect.attributes.typography.letter-spacing")
:value (attr->string letter-spacing)
:placeholder (if (= :multiple letter-spacing) (tr "settings.multiple") "--")
:on-change #(handle-change % :letter-spacing)

View File

@@ -68,7 +68,7 @@
(mf/defc color-token-row*
{::mf/private true}
[{:keys [active-tokens color-token color on-swatch-click-token detach-token open-modal-from-token]}]
[{:keys [active-tokens applied-token-name color on-swatch-click-token detach-token open-modal-from-token]}]
(let [;; `active-tokens` may be provided as a `delay` (lazy computation).
;; In that case we must deref it (`@active-tokens`) to force evaluation
;; and obtain the actual value. If its already realized (not a delay),
@@ -77,21 +77,22 @@
@active-tokens
active-tokens)
color-tokens (:color active-tokens)
active-color-tokens (:color active-tokens)
token (some #(when (= (:name %) color-token) %) color-tokens)
token (some #(when (= (:name %) applied-token-name) %) active-color-tokens)
on-detach-token
(mf/use-fn
(mf/deps detach-token token color-token)
(mf/deps detach-token token applied-token-name)
(fn []
(let [token (or token color-token)]
(let [token (or token applied-token-name)]
(detach-token token))))
has-errors (some? (:errors token))
token-name (:name token)
resolved (:resolved-value token)
not-active (and (some? active-tokens) (nil? token))
not-active (and (empty? active-tokens)
(nil? token))
id (dm/str (:id token) "-name")
swatch-tooltip-content (cond
not-active
@@ -109,7 +110,7 @@
#(mf/html
[:div
[:span (dm/str (tr "workspace.tokens.token-name") ": ")]
[:span {:class (stl/css :token-name-tooltip)} color-token]]))]
[:span {:class (stl/css :token-name-tooltip)} applied-token-name]]))]
[:div {:class (stl/css :color-info)}
[:div {:class (stl/css-case :token-color-wrapper true
@@ -128,7 +129,7 @@
:class (stl/css :token-tooltip)}
[:div {:class (stl/css :token-name)
:aria-labelledby id}
(or token-name color-token)]]
(or token-name applied-token-name)]]
[:div {:class (stl/css :token-actions)}
[:> icon-button*
{:variant "action"
@@ -146,7 +147,11 @@
on-change on-reorder on-detach on-open on-close on-remove origin on-detach-token
disable-drag on-focus on-blur select-only select-on-focus on-token-change applied-token]}]
(let [token-color (contains? cfg/flags :token-color)
(let [;; TODO: Remove this workaround fixing `get-attrs*` fn on sidebar/options/shapes/multiple.cljs
applied-token (if (= :multiple applied-token)
nil
applied-token)
token-color (contains? cfg/flags :token-color)
libraries (mf/deref refs/files)
color-without-hash (mf/use-memo
@@ -177,7 +182,6 @@
(-> (deref active-tokens*)
(select-keys (get tk/tokens-by-input origin))
(not-empty)))))
on-focus'
(mf/use-fn
(mf/deps on-focus)
@@ -352,7 +356,7 @@
(cond
(and token-color applied-token)
[:> color-token-row* {:active-tokens tokens
:color-token applied-token
:applied-token-name applied-token
:color (dissoc color :ref-id :ref-file)
:on-swatch-click-token on-swatch-click-token
:detach-token detach-token

View File

@@ -63,7 +63,8 @@
:data {:index index})
[nil nil])
stroke-color-token (:stroke-color applied-tokens)
stroke-color-token
(:stroke-color applied-tokens)
on-color-change-refactor
(mf/use-fn

View File

@@ -20,15 +20,21 @@
;; Component -------------------------------------------------------------------
(defn calculate-position
(defn- calculate-position
"Calculates the style properties for the given coordinates and position"
[{vh :height} position x y color?]
(let [;; picker height in pixels
;; TODO: Revisit these harcoded values
h (if color? 610 510)
[{vh :height} position x y token-type]
(let [; TODO: Revisit these harcoded values
modal-height (case token-type
:color
500
:typography
660
:shadow
660
400)
;; Checks for overflow outside the viewport height
max-y (- vh h)
overflow-fix (max 0 (+ y (- 50) h (- vh)))
max-y (- vh modal-height)
overflow-fix (max 0 (+ y (- 50) modal-height (- vh)))
bottom-offset "1rem"
top-offset (dm/str (- y 70) "px")
max-height-top (str "calc(100vh - " top-offset)
@@ -61,17 +67,19 @@
:top (dm/str (- y 70 overflow-fix) "px")
:maxHeight max-height-top}))))
(defn use-viewport-position-style [x y position color?]
(defn use-viewport-position-style [x y position token-type]
(let [vport (-> (l/derived :vport refs/workspace-local)
(mf/deref))]
(-> (calculate-position vport position x y color?)
(-> (calculate-position vport position x y token-type)
(clj->js))))
(mf/defc token-update-create-modal
{::mf/wrap-props false}
[{:keys [x y position token token-type action selected-token-set-id] :as _args}]
(let [wrapper-style (use-viewport-position-style x y position (= token-type :color))
modal-size-large* (mf/use-state (= token-type :typography))
(let [wrapper-style (use-viewport-position-style x y position token-type)
modal-size-large* (mf/use-state (or (= token-type :typography)
(= token-type :color)
(= token-type :shadow)))
modal-size-large? (deref modal-size-large*)
close-modal (mf/use-fn
(fn []

View File

@@ -18,7 +18,7 @@
padding: deprecated.$s-8 deprecated.$s-16;
border-radius: deprecated.$s-8;
border: deprecated.$s-2 solid var(--panel-border-color);
z-index: deprecated.$z-index-3;
z-index: deprecated.$z-index-1;
background-color: var(--color-background-primary);
transition:
top 0.3s,

View File

@@ -6,7 +6,6 @@
(ns app.main.ui.workspace.viewport.debug
(:require
[app.render-wasm.api :as wasm.api]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
@@ -276,29 +275,3 @@
:y2 (:y end-p)
:style {:stroke "red"
:stroke-width (/ 1 zoom)}}]))]))))
(mf/defc debug-text-position-data
{::mf/wrap-props false}
[props]
(let [objects (unchecked-get props "objects")
zoom (unchecked-get props "zoom")
selected-shapes (unchecked-get props "selected-shapes")
selected-text
(when (and (= (count selected-shapes) 1) (= :text (-> selected-shapes first :type)))
(first selected-shapes))
position-data
(when selected-text
(wasm.api/calculate-position-data selected-text))]
(for [{:keys [x y width height]} position-data]
[:rect {:x x
:y y
:width width
:height height
:fill "none"
:strokeWidth 1
:stroke "red"}]
)))

View File

@@ -23,7 +23,6 @@
[app.main.data.workspace.grid-layout.editor :as dwge]
[app.main.data.workspace.modifiers :as dwm]
[app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.transforms :as dwt]
[app.main.features :as features]
[app.main.refs :as refs]
[app.main.store :as st]
@@ -258,8 +257,7 @@
(let [modifiers (calculate-drag-modifiers position)
modif-tree (dwm/create-modif-tree [(:id shape)] modifiers)]
(when on-clear-modifiers (on-clear-modifiers modifiers))
(st/emit! (dwm/apply-wasm-modifiers modif-tree)
(dwt/finish-transform)))
(st/emit! (dwm/apply-wasm-modifiers modif-tree)))
(st/emit! (dwm/apply-modifiers)))))
{:keys [handle-pointer-down handle-lost-pointer-capture handle-pointer-move]}
@@ -508,8 +506,7 @@
(let [modifiers (calculate-modifiers position)
modif-tree (dwm/create-modif-tree [(:id shape)] modifiers)]
(when on-clear-modifiers (on-clear-modifiers))
(st/emit! (dwm/apply-wasm-modifiers modif-tree)
(dwt/finish-transform)))
(st/emit! (dwm/apply-wasm-modifiers modif-tree)))
(st/emit! (dwm/apply-modifiers)))
(reset! start-size-before nil)
(reset! start-size-after nil)))]

View File

@@ -54,11 +54,15 @@
[app.util.debug :as dbg]
[app.util.text-editor :as ted]
[beicon.v2.core :as rx]
[okulary.core :as l]
[promesa.core :as p]
[rumext.v2 :as mf]))
;; --- Viewport
(def workspace-wasm-modifiers
(l/derived :workspace-wasm-modifiers st/state))
(defn apply-modifiers-to-selected
[selected objects modifiers]
(->> modifiers
@@ -94,7 +98,7 @@
;; DEREFS
drawing (mf/deref refs/workspace-drawing)
focus (mf/deref refs/workspace-focus-selected)
wasm-modifiers (mf/deref refs/workspace-wasm-modifiers)
wasm-modifiers (mf/deref workspace-wasm-modifiers)
workspace-editor-state (mf/deref refs/workspace-editor-state)
@@ -635,10 +639,6 @@
:hover-top-frame-id @hover-top-frame-id
:zoom zoom}])
[:& wvd/debug-text-position-data {:selected-shapes selected-shapes
:objects base-objects
:zoom zoom}]
(when show-selection-handlers?
[:g.selection-handlers {:clipPath "url(#clip-handlers)"}
(when-not text-editing?

View File

@@ -18,7 +18,6 @@
[app.main.render :as render]
[app.main.repo :as repo]
[app.main.store :as st]
[app.main.ui.context :as ctx]
[app.util.dom :as dom]
[app.util.globals :as glob]
[beicon.v2.core :as rx]
@@ -77,12 +76,11 @@
(mth/ceil height) "px")}))))
(when objects
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}]])))
[:& render/object-svg
{:objects objects
:object-id object-id
:embed embed
:skip-children skip-children}])))
(mf/defc objects-svg
{::mf/wrap-props false}
@@ -90,13 +88,12 @@
(when-let [objects (mf/deref ref:objects)]
(for [object-id object-ids]
(let [objects (render/adapt-objects-for-shape objects object-id)]
[:& (mf/provider ctx/is-render?) {:value true}
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]]))))
[:& render/object-svg
{:objects objects
:key (str object-id)
:object-id object-id
:embed embed
:skip-children skip-children}]))))
(defn- fetch-objects-bundle
[& {:keys [file-id page-id share-id object-id] :as options}]

View File

@@ -7,9 +7,6 @@
(ns app.render-wasm.api
"A WASM based render API"
(:require
[potok.v2.core :as ptk]
[app.main.data.helpers :as dsh]
[app.main.ui.shapes.text]
["react-dom/server" :as rds]
[app.common.data :as d]
[app.common.data.macros :as dm]
@@ -23,6 +20,7 @@
[app.common.types.shape.layout :as ctl]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.fonts :as fonts]
[app.main.refs :as refs]
[app.main.render :as render]
[app.main.store :as st]
@@ -62,9 +60,6 @@
(def ^:const MAX_BUFFER_CHUNK_SIZE (* 256 1024))
(def ^:const DEBOUNCE_DELAY_MS 100)
(def ^:const THROTTLE_DELAY_MS 10)
(def dpr
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
@@ -130,18 +125,10 @@
(render ts)))))
(declare get-text-dimensions)
(declare calculate-position-data)
(defn update-text-rect!
[id]
(when wasm/context-initialized?
(let [objects (dsh/lookup-page-objects @st/state)
shape (get objects id)
position-data (calculate-position-data shape)]
(.log js/console (:name shape) (clj->js position-data))
(st/emit!
(ptk/data-event :wasm/position-data {:id id :position-data position-data})))
(mw/emit!
{:cmd :index/update-text-rect
:page-id (:current-page-id @st/state)
@@ -841,7 +828,7 @@
(set-shape-vertical-align (get content :vertical-align))
(let [fonts (f/get-content-fonts content)
(let [fonts (fonts/get-content-fonts content)
fallback-fonts (fonts-from-text-content content true)
all-fonts (concat fonts fallback-fonts)
result (f/store-fonts shape-id all-fonts)]
@@ -887,10 +874,10 @@
(letfn [(do-render [ts]
(h/call wasm/internal-module "_set_view_end")
(render ts))]
(fns/debounce do-render DEBOUNCE_DELAY_MS)))
(fns/debounce do-render 100)))
(def render-pan
(fns/throttle render THROTTLE_DELAY_MS))
(fns/throttle render 10))
(defn set-view-box
[prev-zoom zoom vbox]
@@ -999,7 +986,10 @@
(run!
(fn [id]
(f/update-text-layout id)
(update-text-rect! id)))))
(mw/emit! {:cmd :index/update-text-rect
:page-id (:current-page-id @st/state)
:shape-id id
:dimensions (get-text-dimensions id)})))))
(defn process-pending
([shapes thumbnails full on-complete]
@@ -1053,7 +1043,6 @@
(process-pending shapes thumbnails full noop-fn
(fn []
(when render-callback (render-callback))
(render-finish)
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode
@@ -1257,15 +1246,9 @@
(defn clear-canvas
[]
(try
;; TODO: perform corresponding cleaning
(set! wasm/context-initialized? false)
(h/call wasm/internal-module "_clean_up")
;; If this calls panics we don't want to crash. This happens sometimes
;; with hot-reload in develop
(catch :default error
(.error js/console error))))
;; TODO: perform corresponding cleaning
(set! wasm/context-initialized? false)
(h/call wasm/internal-module "_clean_up"))
(defn show-grid
[id]
@@ -1308,7 +1291,12 @@
(mem/free)
content))
(defn calculate-bool*
(defn- calculate-bool*
[bool-type]
(-> (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type))
(mem/->offset-32)))
(defn calculate-bool
[bool-type ids]
(let [size (mem/get-alloc-size ids UUID-U8-SIZE)
heap (mem/get-heap-u32)
@@ -1319,10 +1307,7 @@
offset
(rseq ids))
(let [offset
(-> (h/call wasm/internal-module "_calculate_bool" (sr/translate-bool-type bool-type))
(mem/->offset-32))
(let [offset (calculate-bool* bool-type)
length (aget heap offset)
data (mem/slice heap
(+ offset 1)
@@ -1331,126 +1316,16 @@
(mem/free)
content)))
(defn calculate-bool
[shape objects]
;; We need to be able to calculate the boolean data but we cannot
;; depend on the serialization flow.
;; start_temp_object / end_temp_object create a new shapes_pool
;; temporary and then we serialize the objects needed to calculate the
;; boolean object.
;; After the content is returned we discard that temporary context
(h/call wasm/internal-module "_start_temp_objects")
(let [bool-type (get shape :bool-type)
ids (get shape :shapes)
all-children
(->> ids
(mapcat #(cfh/get-children-with-self objects %)))]
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
(run! (partial set-object objects) all-children)
(let [content (-> (calculate-bool* bool-type ids)
(path.impl/path-data))]
(h/call wasm/internal-module "_end_temp_objects")
content)))
(def POSITION-DATA-U8-SIZE 36)
(def POSITION-DATA-U32-SIZE (/ POSITION-DATA-U8-SIZE 4))
(defn calculate-position-data
[shape]
(use-shape (:id shape))
(let [heapf32 (mem/get-heap-f32)
heapu32 (mem/get-heap-u32)
offset (-> (h/call wasm/internal-module "_calc_position_data")
(mem/->offset-32))
length (aget heapu32 offset)
max-offset (+ offset 1 (* length POSITION-DATA-U32-SIZE))
result
(loop [result (transient [])
offset (inc offset)]
(if (< offset max-offset)
(let [entry (dr/read-position-data-entry heapu32 heapf32 offset)]
(recur (conj! result entry)
(+ offset POSITION-DATA-U32-SIZE)))
(persistent! result)))
result
(->> result
(mapv
(fn [{:keys [paragraph span start-pos end-pos direction x y width height]}]
(let [content (:content shape)
element (-> content :children
(get 0) :children ;; paragraph-set
(get paragraph) :children ;; paragraph
(get span))
text (subs (:text element) start-pos end-pos)]
{:x x
:y y
:width width
:height height
:direction direction
:font-family (get element :font-family)
:font-size (get element :font-size)
:font-weight (get element :font-weight)
:text-transform (get element :text-transform)
:text-decoration (get element :text-decoration)
:letter-spacing (get element :letter-spacing)
:font-style (get element :font-style)
:fills (get element :fills)
:text text}))))]
(mem/free)
result))
(defn init-wasm-module
[module]
(let [default-fn (unchecked-get module "default")
serializers
#js
{:blur-type (unchecked-get module "RawBlurType")
:blend-mode (unchecked-get module "RawBlendMode")
:bool-type (unchecked-get module "RawBoolType")
:font-style (unchecked-get module "RawFontStyle")
:flex-direction (unchecked-get module "RawFlexDirection")
:grid-direction (unchecked-get module "RawGridDirection")
:grow-type (unchecked-get module "RawGrowType")
:align-items (unchecked-get module "RawAlignItems")
:align-self (unchecked-get module "RawAlignSelf")
:align-content (unchecked-get module "RawAlignContent")
:justify-items (unchecked-get module "RawJustifyItems")
:justify-content (unchecked-get module "RawJustifyContent")
:justify-self (unchecked-get module "RawJustifySelf")
:wrap-type (unchecked-get module "RawWrapType")
:grid-track-type (unchecked-get module "RawGridTrackType")
:shadow-style (unchecked-get module "RawShadowStyle")
:stroke-style (unchecked-get module "RawStrokeStyle")
:stroke-cap (unchecked-get module "RawStrokeCap")
:shape-type (unchecked-get module "RawShapeType")
:constraint-h (unchecked-get module "RawConstraintH")
:constraint-v (unchecked-get module "RawConstraintV")
:sizing (unchecked-get module "RawSizing")
:vertical-align (unchecked-get module "RawVerticalAlign")
:fill-data (unchecked-get module "RawFillData")
:text-align (unchecked-get module "RawTextAlign")
:text-direction (unchecked-get module "RawTextDirection")
:text-decoration (unchecked-get module "RawTextDecoration")
:text-transform (unchecked-get module "RawTextTransform")
:segment-data (unchecked-get module "RawSegmentData")
:stroke-linecap (unchecked-get module "RawStrokeLineCap")
:stroke-linejoin (unchecked-get module "RawStrokeLineJoin")
:fill-rule (unchecked-get module "RawFillRule")}]
(set! wasm/serializers serializers)
(default-fn)))
href (cf/resolve-href "js/render-wasm.wasm")]
(default-fn #js {:locateFile (constantly href)})))
(defonce module
(delay
(if (exists? js/dynamicImport)
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(let [uri (cf/resolve-href "js/render-wasm.js")]
(->> (js/dynamicImport (str uri))
(p/mcat init-wasm-module)
(p/fmap

View File

@@ -9,7 +9,6 @@
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.logging :as log]
[app.common.types.text :as txt]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.fonts :as fonts]
@@ -50,13 +49,10 @@
:builtin))
(defn- font-db-data
[font-id font-variant-id font-weight-fallback font-style-fallback]
[font-id font-variant-id]
(let [font (fonts/get-font-data font-id)
closest-variant (fonts/find-closest-variant font font-weight-fallback font-style-fallback)
variant (fonts/get-variant font font-variant-id)]
(if (or (nil? closest-variant) (= closest-variant variant))
variant
closest-variant)))
variant))
(defn- font-id->uuid [font-id]
(case (font-backend font-id)
@@ -67,22 +63,22 @@
:builtin
uuid/zero))
(defn ^:private font-id->asset-id [font-id font-variant-id font-weight font-style]
(defn ^:private font-id->asset-id [font-id font-variant-id]
(case (font-backend font-id)
:google
font-id
:custom
(let [font-uuid (custom-font-id->uuid font-id)
matching-font (some (fn [[_ font]]
(and (= (:font-id font) font-uuid)
(= (str (:font-weight font)) (str font-weight))
font))
(seq @fonts))]
matching-font (d/seek (fn [[_ font]]
(let [variant-id (or (:font-variant-id font) (dm/str (:font-style font) "-" (:font-weight font)))]
(and (= (:font-id font) font-uuid)
(or (nil? font-variant-id)
(= variant-id font-variant-id)))))
(seq @fonts))]
(when matching-font
(:ttf-file-id matching-font)))
(:ttf-file-id (second matching-font))))
:builtin
(let [variant (font-db-data font-id font-variant-id font-weight font-style)]
(let [variant (font-db-data font-id font-variant-id)]
(:ttf-url variant))))
(defn update-text-layout
@@ -104,7 +100,6 @@
ptr (h/call wasm/internal-module "_alloc_bytes" size)
heap (gobj/get ^js wasm/internal-module "HEAPU8")
mem (js/Uint8Array. (.-buffer heap) ptr size)]
(.set mem (js/Uint8Array. font-array-buffer))
(h/call wasm/internal-module "_store_font"
(aget shape-id-buffer 0)
@@ -139,17 +134,17 @@
(rx/empty))))})
(defn- google-font-ttf-url
[font-id font-variant-id font-weight font-style]
(let [variant (font-db-data font-id font-variant-id font-weight font-style)]
[font-id font-variant-id]
(let [variant (font-db-data font-id font-variant-id)]
(if-let [ttf-url (:ttf-url variant)]
(str/replace ttf-url "https://fonts.gstatic.com/s/" (u/join cf/public-uri "/internal/gfonts/font/"))
nil)))
(defn- font-id->ttf-url
[font-id asset-id font-variant-id font-weight font-style]
[font-id asset-id font-variant-id]
(case (font-backend font-id)
:google
(google-font-ttf-url font-id font-variant-id font-weight font-style)
(google-font-ttf-url font-id font-variant-id)
:custom
(dm/str (u/join cf/public-uri "assets/by-id/" asset-id))
:builtin
@@ -158,7 +153,7 @@
(defn- store-font-id
[shape-id font-data asset-id emoji? fallback?]
(when asset-id
(let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data) (:weight font-data) (:style-name font-data))
(let [uri (font-id->ttf-url (:font-id font-data) asset-id (:font-variant-id font-data))
id-buffer (uuid/get-u32 (:wasm-id font-data))
font-data (assoc font-data :family-id-buffer id-buffer)
font-stored? (not= 0 (h/call wasm/internal-module "_is_font_uploaded"
@@ -192,30 +187,6 @@
(catch :default _e
uuid/zero)))
(defn normalize-span-font
[span paragraph]
(let [font-id (:font-id span)
font-variant-id (:font-variant-id span)
font-weight-fallback (or (:font-weight span) (:font-weight paragraph))
font-style-fallback (or (:font-style span) (:font-style paragraph))
font-data (font-db-data font-id font-variant-id font-weight-fallback font-style-fallback)]
(-> span
(assoc :font-variant-id (or (:id font-data) (:id font-data) font-variant-id)
:font-weight (or (:weight font-data) font-weight-fallback)
:font-style (or (:style font-data) font-style-fallback)))))
(defn normalize-paragraph-font
[paragraph]
(let [font-id (:font-id paragraph)
font-variant-id (:font-variant-id paragraph)
font-weight-fallback (:font-weight paragraph)
font-style-fallback (:font-style paragraph)
font-data (font-db-data font-id font-variant-id font-weight-fallback font-style-fallback)]
(-> paragraph
(assoc :font-variant-id (or (:id font-data) (:id font-data) font-variant-id)
:font-weight (or (:weight font-data) font-weight-fallback)
:font-style (or (:style font-data) font-style-fallback)))))
(defn serialize-font-size
[font-size]
(cond
@@ -273,41 +244,26 @@
(string? letter-spacing)
(or (d/parse-double letter-spacing) default-letter-spacing)))
(defn normalize-font-variant
[font-variant-id]
(if (or (nil? font-variant-id) (str/blank? font-variant-id))
"regular"
font-variant-id))
(defn store-font
[shape-id font]
(let [font-id (get font :font-id)
font-variant-id (get font :font-variant-id)
normalized-variant-id (when font-variant-id
(-> font-variant-id
(str/lower)
(str/replace #"\s+" "")))
font-weight-fallback (or (get font :font-weight) 400)
font-style-fallback (or (get font :font-style) "normal")
emoji? (get font :is-emoji false)
fallback? (get font :is-fallback false)
font-data (font-db-data font-id normalized-variant-id font-weight-fallback font-style-fallback)
wasm-id (font-id->uuid font-id)
raw-weight (or (:weight font-data) font-weight-fallback)
raw-weight (or (:weight (font-db-data font-id font-variant-id)) 400)
weight (serialize-font-weight raw-weight)
style (cond
(str/includes? (or normalized-variant-id "") "italic") "italic"
(str/includes? raw-weight "italic") "italic"
:else font-style-fallback)
variant-id (or (:id font-data) normalized-variant-id)
asset-id (font-id->asset-id font-id variant-id raw-weight style)
style (serialize-font-style (cond
(str/includes? font-variant-id "italic") "italic"
(str/includes? raw-weight "italic") "italic"
:else "normal"))
asset-id (font-id->asset-id font-id font-variant-id)
font-data {:wasm-id wasm-id
:font-id font-id
:font-variant-id variant-id
:style (serialize-font-style style)
:style-name style
:font-variant-id font-variant-id
:style style
:weight weight}]
(store-font-id shape-id font-data asset-id emoji? fallback?)))
;; FIXME: This is a temporary function to load the fallback fonts for the editor.
@@ -317,29 +273,6 @@
(doseq [font fonts]
(fonts/ensure-loaded! (:font-id font) (:font-variant-id font))))
(defn get-content-fonts
"Extends from app.main.fonts/get-content-fonts. Extracts the fonts used by the content of a text shape, resolving the correct font variant info."
[content]
(let [paragraph-set (first (get content :children))
paragraphs (get paragraph-set :children)]
(->> paragraphs
(mapcat #(get % :children))
(filter txt/is-text-node?)
(reduce
(fn [result {:keys [font-id font-variant-id font-weight font-style] :as node}]
(let [resolved-font-id (or font-id (:font-id txt/default-typography))
resolved-variant-id (or font-variant-id (:font-variant-id txt/default-typography))
font-weight-fallback (or font-weight (:font-weight txt/default-typography) 400)
font-style-fallback (or font-style (:font-style txt/default-typography) "normal")
font-data (font-db-data resolved-font-id resolved-variant-id font-weight-fallback font-style-fallback)
font-ref {:font-id resolved-font-id
:font-variant-id (or (:id font-data) (:name font-data) resolved-variant-id)
:font-weight (or (:weight font-data) font-weight-fallback)
:font-style (or (:style font-data) font-style-fallback)}]
(conj result font-ref)))
#{}))))
(defn store-fonts
[shape-id fonts]
(keep (fn [font] (store-font shape-id font)) fonts))

View File

@@ -0,0 +1,242 @@
export const GrowType = {
"fixed": 0,
"auto-width": 1,
"auto-height": 2,
};
export const RawBlendMode = {
"normal": 3,
"screen": 14,
"overlay": 15,
"darken": 16,
"lighten": 17,
"color-dodge": 18,
"color-burn": 19,
"hard-light": 20,
"soft-light": 21,
"difference": 22,
"exclusion": 23,
"multiply": 24,
"hue": 25,
"saturation": 26,
"color": 27,
"luminosity": 28,
};
export const RawBlurType = {
"layer-blur": 0,
};
export const RawFillData = {
"solid": 0,
"linear": 1,
"radial": 2,
"image": 3,
};
export const RawFontStyle = {
"normal": 0,
"italic": 1,
};
export const RawAlignItems = {
"start": 0,
"end": 1,
"center": 2,
"stretch": 3,
};
export const RawAlignContent = {
"start": 0,
"end": 1,
"center": 2,
"space-between": 3,
"space-around": 4,
"space-evenly": 5,
"stretch": 6,
};
export const RawJustifyItems = {
"start": 0,
"end": 1,
"center": 2,
"stretch": 3,
};
export const RawJustifyContent = {
"start": 0,
"end": 1,
"center": 2,
"space-between": 3,
"space-around": 4,
"space-evenly": 5,
"stretch": 6,
};
export const RawJustifySelf = {
"none": 0,
"auto": 1,
"start": 2,
"end": 3,
"center": 4,
"stretch": 5,
};
export const RawAlignSelf = {
"none": 0,
"auto": 1,
"start": 2,
"end": 3,
"center": 4,
"stretch": 5,
};
export const RawVerticalAlign = {
"top": 0,
"center": 1,
"bottom": 2,
};
export const RawConstraintH = {
"left": 0,
"right": 1,
"leftright": 2,
"center": 3,
"scale": 4,
};
export const RawConstraintV = {
"top": 0,
"bottom": 1,
"topbottom": 2,
"center": 3,
"scale": 4,
};
export const RawFlexDirection = {
"row": 0,
"row-reverse": 1,
"column": 2,
"column-reverse": 3,
};
export const RawWrapType = {
"wrap": 0,
"nowrap": 1,
};
export const RawGridDirection = {
"row": 0,
"column": 1,
};
export const RawGridTrackType = {
"percent": 0,
"flex": 1,
"auto": 2,
"fixed": 3,
};
export const RawSizing = {
"fill": 0,
"fix": 1,
"auto": 2,
};
export const RawBoolType = {
"union": 0,
"difference": 1,
"intersection": 2,
"exclusion": 3,
};
export const RawSegmentData = {
"move-to": 1,
"line-to": 2,
"curve-to": 3,
"close": 4,
};
export const RawShadowStyle = {
"drop-shadow": 0,
"inner-shadow": 1,
};
export const RawShapeType = {
"frame": 0,
"group": 1,
"bool": 2,
"rect": 3,
"path": 4,
"text": 5,
"circle": 6,
"svg-raw": 7,
};
export const RawStrokeStyle = {
"solid": 0,
"dotted": 1,
"dashed": 2,
"mixed": 3,
};
export const RawStrokeCap = {
"none": 0,
"line-arrow": 1,
"triangle-arrow": 2,
"square-marker": 3,
"circle-marker": 4,
"diamond-marker": 5,
"round": 6,
"square": 7,
};
export const RawFillRule = {
"nonzero": 0,
"evenodd": 1,
};
export const RawStrokeLineCap = {
"butt": 0,
"round": 1,
"square": 2,
};
export const RawStrokeLineJoin = {
"miter": 0,
"round": 1,
"bevel": 2,
};
export const RawTextAlign = {
"left": 0,
"center": 1,
"right": 2,
"justify": 3,
};
export const RawTextDirection = {
"ltr": 0,
"rtl": 1,
};
export const RawTextDecoration = {
"none": 0,
"underline": 1,
"line-through": 2,
"overline": 3,
};
export const RawTextTransform = {
"none": 0,
"uppercase": 1,
"lowercase": 2,
"capitalize": 3,
};
export const RawGrowType = {
"fixed": 0,
"auto-width": 1,
"auto-height": 2,
};

View File

@@ -80,7 +80,7 @@
font-size (f/serialize-font-size font-size)
line-height (f/serialize-line-height (get span :line-height) paragraph-line-height)
letter-spacing (f/serialize-letter-spacing (get span :letter-spacing))
letter-spacing (f/serialize-letter-spacing (get paragraph :letter-spacing))
font-weight (get span :font-weight paragraph-font-weight)
font-weight (f/serialize-font-weight font-weight)
@@ -142,9 +142,7 @@
;; buffer has the following format:
;; [<num-spans> <paragraph_attributes> <spans_attributes> <text>]
[spans paragraph text]
(let [normalized-paragraph (f/normalize-paragraph-font paragraph)
normalized-spans (map #(f/normalize-span-font % normalized-paragraph) spans)
num-spans (count normalized-spans)
(let [num-spans (count spans)
fills-size (* types.fills.impl/FILL-U8-SIZE MAX-TEXT-FILLS)
metadata-size (+ PARAGRAPH-ATTR-U8-SIZE
(* num-spans (+ SPAN-ATTR-U8-SIZE fills-size)))
@@ -159,8 +157,8 @@
(-> offset
(mem/write-u32 dview num-spans)
(write-paragraph dview normalized-paragraph)
(write-spans dview normalized-spans normalized-paragraph)
(write-paragraph dview paragraph)
(write-spans dview spans paragraph)
(mem/write-buffer heapu8 text-buffer))
(h/call wasm/internal-module "_set_shape_text_content")))

View File

@@ -45,23 +45,4 @@
:center (gpt/point cx cy)
:transform (gmt/matrix a b c d e f)}))
(defn read-position-data-entry
[heapu32 heapf32 offset]
(let [paragraph (aget heapu32 (+ offset 0))
span (aget heapu32 (+ offset 1))
start-pos (aget heapu32 (+ offset 2))
end-pos (aget heapu32 (+ offset 3))
x (aget heapf32 (+ offset 4))
y (aget heapf32 (+ offset 5))
width (aget heapf32 (+ offset 6))
height (aget heapf32 (+ offset 7))
direction (aget heapu32 (+ offset 8))]
{:paragraph paragraph
:span span
:start-pos start-pos
:end-pos end-pos
:x x
:y y
:width width
:height height
:direction direction}))

View File

@@ -291,8 +291,7 @@
(api/set-grid-layout-data shape)
(ctl/flex-layout? shape)
(api/set-flex-layout shape))
(api/set-layout-child shape))
(api/set-flex-layout shape)))
;; Property not in WASM
nil))))
@@ -323,7 +322,7 @@
(rx/subs! #(api/request-render "set-wasm-attrs"))))
;; `conj` empty set initialization
(def conj* (fnil conj (d/ordered-set)))
(def conj* (fnil conj #{}))
(defn- impl-assoc
[self k v]

View File

@@ -4,9 +4,43 @@
;;
;; Copyright (c) KALEIDOS INC
(ns app.render-wasm.wasm)
(ns app.render-wasm.wasm
(:require ["./api/shared.js" :as shared]))
(defonce internal-frame-id nil)
(defonce internal-module #js {})
(defonce serializers #js {})
(defonce serializers
#js {:blur-type shared/RawBlurType
:blend-mode shared/RawBlendMode
:bool-type shared/RawBoolType
:font-style shared/RawFontStyle
:flex-direction shared/RawFlexDirection
:grid-direction shared/RawGridDirection
:grow-type shared/RawGrowType
:align-items shared/RawAlignItems
:align-self shared/RawAlignSelf
:align-content shared/RawAlignContent
:justify-items shared/RawJustifyItems
:justify-content shared/RawJustifyContent
:justify-self shared/RawJustifySelf
:wrap-type shared/RawWrapType
:grid-track-type shared/RawGridTrackType
:shadow-style shared/RawShadowStyle
:stroke-style shared/RawStrokeStyle
:stroke-cap shared/RawStrokeCap
:shape-type shared/RawShapeType
:constraint-h shared/RawConstraintH
:constraint-v shared/RawConstraintV
:sizing shared/RawSizing
:vertical-align shared/RawVerticalAlign
:fill-data shared/RawFillData
:text-align shared/RawTextAlign
:text-direction shared/RawTextDirection
:text-decoration shared/RawTextDecoration
:text-transform shared/RawTextTransform
:segment-data shared/RawSegmentData
:stroke-linecap shared/RawStrokeLineCap
:stroke-linejoin shared/RawStrokeLineJoin
:fill-rule shared/RawFillRule})
(defonce context-initialized? false)

View File

@@ -48,9 +48,6 @@
"This function strips units from attr values and un-scapes font-family"
[k v]
(cond
(= v "mixed")
:multiple
(and (or (= k :font-size)
(= k :letter-spacing))
(= (str/slice v -2) "px"))

View File

@@ -89,7 +89,7 @@
(defn init
"Return a initialized webworker instance."
[path on-error]
(let [instance (js/Worker. path #js {:type "module"})
(let [instance (js/Worker. path)
bus (rx/subject)
worker (Worker. instance (rx/to-observable bus))

View File

@@ -257,7 +257,7 @@
(filter (if clip-children?
(comp overlaps-parent? :clip-parents)
(constantly true)))
(keep :id))
(map :id))
result)))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -24,11 +24,12 @@
[beicon.v2.core :as rx]
[okulary.core :as l]
[promesa.core :as p]
[rumext.v2 :as mf]
[shadow.esm :refer (dynamic-import)]))
[rumext.v2 :as mf]))
(log/set-level! :trace)
(def ^:private ^:const thumbnail-aspect-ratio (/ 2 3))
(defn- handle-response
[{:keys [body status] :as response}]
(cond
@@ -64,6 +65,10 @@
(rx/map http/conditional-decode-transit)
(rx/mapcat handle-response))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; SVG RENDERING (LEGACY RENDER)
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- render-thumbnail
[{:keys [page file-id revn] :as params}]
(try
@@ -98,15 +103,13 @@
(->> (request-data-for-thumbnail file-id revn true)
(rx/map render-thumbnail)))
(def init-wasm
(delay
(let [uri (cf/resolve-static-asset "js/render_wasm.js")]
(-> (dynamic-import (str uri))
(p/then #(wasm.api/init-wasm-module %))
(p/then #(set! wasm/internal-module %))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; WASM RENDERING
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(mf/defc svg-wrapper
[{:keys [data-uri background width height]}]
(mf/defc svg-wrapper*
{::mf/private true}
[{:keys [uri background width height]}]
[:svg {:version "1.1"
:xmlns "http://www.w3.org/2000/svg"
:xmlnsXlink "http://www.w3.org/1999/xlink"
@@ -116,85 +119,97 @@
:background background}
:fill "none"
:viewBox (dm/str "0 0 " width " " height)}
[:image {:xlinkHref data-uri
[:image {:xlinkHref uri
:width width
:height height}]])
(defn blob->uri
(defn- blob->uri
[blob]
(.readAsDataURL (js/FileReaderSync.) blob))
(def thumbnail-aspect-ratio (/ 2 3))
(defn- render-canvas-blob
[canvas width height background]
(->> (.convertToBlob ^js canvas)
(p/fmap (fn [blob]
(rds/renderToStaticMarkup
(mf/element svg-wrapper*
#js {:uri (blob->uri blob)
:width width
:height height
:background background}))))))
(defn render-canvas-blob
[canvas width height background-color]
(-> (.convertToBlob canvas)
(p/then
(fn [blob]
(rds/renderToStaticMarkup
(mf/element
svg-wrapper
#js {:data-uri (blob->uri blob)
:width width
:height height
:background background-color}))))))
(defonce ^:private wasm-module
(delay
(let [module (unchecked-get js/globalThis "WasmModule")
init-fn (unchecked-get module "default")
href (cf/resolve-href "js/render-wasm.wasm")]
(->> (init-fn #js {:locateFile (constantly href)})
(p/fnly (fn [module cause]
(if cause
(js/console.error cause)
(set! wasm/internal-module module))))))))
(defn process-wasm-thumbnail
(defn- render-thumbnail-with-wasm
[{:keys [id file-id revn width] :as message}]
(->> (rx/from @init-wasm)
(->> (rx/from @wasm-module)
(rx/mapcat #(request-data-for-thumbnail file-id revn false))
(rx/mapcat
(fn [{:keys [page] :as file}]
(rx/create
(fn [subs]
(let [background-color (or (:background page) cc/canvas)
height (* width thumbnail-aspect-ratio)
canvas (js/OffscreenCanvas. width height)
init? (wasm.api/init-canvas-context canvas)]
(let [bgcolor (or (:background page) cc/canvas)
height (* width thumbnail-aspect-ratio)
canvas (js/OffscreenCanvas. width height)
init? (wasm.api/init-canvas-context canvas)]
(if init?
(let [objects (:objects page)
frame (some->> page :thumbnail-frame-id (get objects))
vbox (if frame
(-> (gsb/get-object-bounds objects frame)
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
(render/calculate-dimensions objects thumbnail-aspect-ratio))
zoom (/ width (:width vbox))]
frame (some->> page :thumbnail-frame-id (get objects))
vbox (if frame
(-> (gsb/get-object-bounds objects frame)
(grc/fix-aspect-ratio thumbnail-aspect-ratio))
(render/calculate-dimensions objects thumbnail-aspect-ratio))
zoom (/ width (:width vbox))]
(wasm.api/initialize-viewport
objects zoom vbox background-color
objects zoom vbox bgcolor
(fn []
(if frame
(wasm.api/render-sync-shape (:id frame))
(wasm.api/render-sync))
(-> (render-canvas-blob canvas width height background-color)
(p/then #(rx/push! subs {:id id :data % :file-id file-id :revn revn}))
(p/catch #(rx/error! subs %))
(p/finally #(rx/end! subs))))))
(->> (render-canvas-blob canvas width height bgcolor)
(p/fnly (fn [data cause]
(if cause
(rx/error! subs cause)
(rx/push! subs
{:id id
:data data
:file-id file-id
:revn revn}))
(rx/end! subs)))))))
(rx/end! subs))
nil)))))))
(defonce thumbs-subject (rx/subject))
(defonce ^:private
thumbnails-queue
(rx/subject))
(defonce thumbs-stream
(->> thumbs-subject
(rx/mapcat process-wasm-thumbnail)
(defonce ^:private
thumbnails-stream
(->> thumbnails-queue
(rx/mapcat render-thumbnail-with-wasm)
(rx/share)))
(defmethod impl/handler :thumbnails/generate-for-file-wasm
[message _]
(rx/create
(fn [subs]
(let [id (uuid/next)
sid
(->> thumbs-stream
(rx/filter #(= id (:id %)))
(rx/subs!
#(do
(rx/push! subs %)
(rx/end! subs))))]
(rx/push! thumbs-subject (assoc message :id id))
(let [id (uuid/next)
sid (->> thumbnails-stream
(rx/filter #(= id (:id %)))
(rx/subs!
(fn [result]
(rx/push! subs result)
(rx/end! subs))))]
(rx/push! thumbnails-queue (assoc message :id id))
#(rx/dispose! sid)))))

View File

@@ -6,7 +6,6 @@
(ns debug
(:require
[app.render-wasm.api :as wasm.api]
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.repair :as cfr]
@@ -457,10 +456,3 @@
(defn ^:export network-averages
[]
(.log js/console (clj->js @http/network-averages)))
(defn ^:export tmp
[]
(let [objects (dsh/lookup-page-objects @st/state)
shape (->> (get-selected @st/state) (first) (get objects))]
(wasm.api/calculate-position-data shape))
)

View File

@@ -26,7 +26,6 @@ import LayoutType from "./layout/LayoutType.js";
* @typedef {Object} TextEditorOptions
* @property {CSSStyleDeclaration|Object.<string,*>} [styleDefaults]
* @property {SelectionControllerDebug} [debug]
* @property {boolean} [shouldUpdatePositionOnScroll=false]
* @property {boolean} [allowHTMLPaste=false]
*/
@@ -93,21 +92,6 @@ export class TextEditor extends EventTarget {
*/
#canvas = null;
/**
* Text editor options.
*
* @type {TextEditorOptions}
*/
#options = {};
/**
* A boolean indicating that this instance was
* disposed or not.
*
* @type {boolean}
*/
#isDisposed = false;
/**
* Constructor.
*
@@ -117,9 +101,9 @@ export class TextEditor extends EventTarget {
*/
constructor(element, canvas, options) {
super();
if (!(element instanceof HTMLElement)) {
if (!(element instanceof HTMLElement))
throw new TypeError("Invalid text editor element");
}
this.#element = element;
this.#canvas = canvas;
this.#events = {
@@ -135,7 +119,6 @@ export class TextEditor extends EventTarget {
keydown: this.#onKeyDown,
};
this.#styleDefaults = options?.styleDefaults;
this.#options = options;
this.#setup(options);
}
@@ -167,18 +150,14 @@ export class TextEditor extends EventTarget {
/**
* Setups the root element.
*
* @param {TextEditorOptions} options
*/
#setupRoot(options) {
#setupRoot() {
this.#root = createEmptyRoot(this.#styleDefaults);
this.#element.appendChild(this.#root);
}
/**
* Setups event listeners.
*
* @param {TextEditorOptions} options
*/
#setupListeners(options) {
this.#changeController.addEventListener("change", this.#onChange);
@@ -194,54 +173,6 @@ export class TextEditor extends EventTarget {
});
}
/**
* Disposes everything.
*/
dispose() {
if (this.#isDisposed) {
return this;
}
this.#isDisposed = true;
// Dispose change controller.
this.#changeController.removeEventListener("change", this.#onChange);
this.#changeController.dispose();
this.#changeController = null;
// Disposes selection controller.
this.#selectionController.removeEventListener(
"stylechange",
this.#onStyleChange,
);
this.#selectionController.dispose();
this.#selectionController = null;
// Disposes the rest of event listeners.
removeEventListeners(this.#element, this.#events);
if (this.#options.shouldUpdatePositionOnScroll) {
window.removeEventListener("scroll", this.#onScroll);
}
// Disposes references to DOM elements.
this.#element = null;
this.#root = null;
return this;
}
/**
* Setups controllers.
*
* @param {TextEditorOptions} options
*/
#setupControllers(options) {
this.#changeController = new ChangeController(this);
this.#selectionController = new SelectionController(
this,
document.getSelection(),
options,
);
}
/**
* Setups the elements, the properties and the
* initial content.
@@ -249,7 +180,12 @@ export class TextEditor extends EventTarget {
#setup(options) {
this.#setupElementProperties(options);
this.#setupRoot(options);
this.#setupControllers(options);
this.#changeController = new ChangeController(this);
this.#selectionController = new SelectionController(
this,
document.getSelection(),
options,
);
this.#setupListeners(options);
}
@@ -306,9 +242,7 @@ export class TextEditor extends EventTarget {
* @param {CustomEvent} e
* @returns {void}
*/
#onChange = (e) => {
this.dispatchEvent(new e.constructor(e.type, e));
};
#onChange = (e) => this.dispatchEvent(new e.constructor(e.type, e));
/**
* Dispatchs a `stylechange` event.
@@ -487,15 +421,6 @@ export class TextEditor extends EventTarget {
);
}
/**
* Indicates that the TextEditor was disposed.
*
* @type {boolean}
*/
get isDisposed() {
return this.#isDisposed;
}
/**
* Root element that contains all the paragraphs.
*
@@ -553,15 +478,6 @@ export class TextEditor extends EventTarget {
return this.#selectionController.currentStyle;
}
/**
* Text editor options
*
* @type {TextEditorOptions}
*/
get options() {
return this.#options;
}
/**
* Focus the element
*/
@@ -624,8 +540,7 @@ export class TextEditor extends EventTarget {
* Applies the current styles to the selection or
* the current DOM node at the caret.
*
* @param {Object.<string, *>} styles
* @returns {TextEditor}
* @param {*} styles
*/
applyStylesToSelection(styles) {
this.#selectionController.startMutation();
@@ -638,8 +553,6 @@ export class TextEditor extends EventTarget {
/**
* Selects all content.
*
* @returns {TextEditor}
*/
selectAll() {
this.#selectionController.selectAll();
@@ -649,12 +562,30 @@ export class TextEditor extends EventTarget {
/**
* Moves cursor to end.
*
* @returns {TextEditor}
* @returns
*/
cursorToEnd() {
this.#selectionController.cursorToEnd();
return this;
}
/**
* Disposes everything.
*/
dispose() {
this.#changeController.removeEventListener("change", this.#onChange);
this.#changeController.dispose();
this.#changeController = null;
this.#selectionController.removeEventListener(
"stylechange",
this.#onStyleChange,
);
this.#selectionController.dispose();
this.#selectionController = null;
removeEventListeners(this.#element, this.#events);
this.#element = null;
this.#root = null;
}
}
/**
@@ -684,98 +615,47 @@ export function createRootFromString(string) {
return root;
}
/**
* Returns true if the passed object is a TextEditor
* instance.
*
* @param {*} instance
* @returns {boolean}
*/
export function isTextEditor(instance) {
export function isEditor(instance) {
return instance instanceof TextEditor;
}
/**
* Returns the root element of a TextEditor
* instance.
*
* @param {TextEditor} instance
* @returns {HTMLDivElement}
*/
/* Convenience function based API for Text Editor */
export function getRoot(instance) {
if (isTextEditor(instance)) {
if (isEditor(instance)) {
return instance.root;
} else {
return null;
}
return null;
}
/**
* Sets the root of the text editor.
*
* @param {TextEditor} instance
* @param {HTMLDivElement} root
* @returns {TextEditor}
*/
export function setRoot(instance, root) {
if (isTextEditor(instance)) {
if (isEditor(instance)) {
instance.root = root;
}
return instance;
}
/**
* Creates a new TextEditor instance.
*
* @param {HTMLDivElement} element
* @param {HTMLCanvasElement} canvas
* @param {TextEditorOptions} options
* @returns {TextEditor}
*/
export function create(element, canvas, options) {
return new TextEditor(element, canvas, { ...options });
}
/**
* Returns the current style of the TextEditor instance.
*
* @param {TextEditor} instance
* @returns {CSSStyleDeclaration|undefined}
*/
export function getCurrentStyle(instance) {
if (isTextEditor(instance)) {
if (isEditor(instance)) {
return instance.currentStyle;
}
return null;
}
/**
* Applies the specified styles to the TextEditor
* passed.
*
* @param {TextEditor} instance
* @param {Object.<string, *>} styles
* @returns {TextEditor|null}
*/
export function applyStylesToSelection(instance, styles) {
if (isTextEditor(instance)) {
if (isEditor(instance)) {
return instance.applyStylesToSelection(styles);
}
return null;
}
/**
* Disposes the current instance resources by nullifying
* every property.
*
* @param {TextEditor} instance
* @returns {TextEditor|null}
*/
export function dispose(instance) {
if (isTextEditor(instance)) {
return instance.dispose();
if (isEditor(instance)) {
instance.dispose();
}
return null;
}
export default TextEditor;

View File

@@ -10,7 +10,6 @@ import {
mapContentFragmentFromHTML,
mapContentFragmentFromString,
} from "../content/dom/Content.js";
import { TextEditor } from "../TextEditor.js";
/**
* Returns a DocumentFragment from text/html.
@@ -39,26 +38,19 @@ function getPlainFragmentFromClipboardData(selectionController, clipboardData) {
}
/**
* Returns a document fragment of html data.
* Returns a DocumentFragment (or null) if it contains
* a compatible clipboardData type.
*
* @param {DataTransfer} clipboardData
* @returns {DocumentFragment}
* @returns {DocumentFragment|null}
*/
function getFormattedOrPlainFragmentFromClipboardData(
selectionController,
clipboardData,
) {
function getFragmentFromClipboardData(selectionController, clipboardData) {
if (clipboardData.types.includes("text/html")) {
return getFormattedFragmentFromClipboardData(
selectionController,
clipboardData,
);
return getFormattedFragmentFromClipboardData(selectionController, clipboardData)
} else if (clipboardData.types.includes("text/plain")) {
return getPlainFragmentFromClipboardData(
selectionController,
clipboardData,
);
return getPlainFragmentFromClipboardData(selectionController, clipboardData)
}
return null
}
/**
@@ -79,37 +71,18 @@ export function paste(event, editor, selectionController) {
let fragment = null;
if (editor?.options?.allowHTMLPaste) {
fragment = getFormattedOrPlainFragmentFromClipboardData(event.clipboardData);
fragment = getFragmentFromClipboardData(selectionController, event.clipboardData);
} else {
fragment = getPlainFragmentFromClipboardData(selectionController, event.clipboardData);
}
if (!fragment) {
// NOOP
return;
}
if (selectionController.isCollapsed) {
const hasOnlyOneParagraph = fragment.children.length === 1;
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
if (hasOnlyOneParagraph
&& hasOnlyOneTextSpan
&& forceTextSpan) {
selectionController.insertIntoFocus(fragment.textContent);
} else {
selectionController.insertPaste(fragment);
}
selectionController.insertPaste(fragment);
} else {
const hasOnlyOneParagraph = fragment.children.length === 1;
const hasOnlyOneTextSpan = fragment.firstElementChild.children.length === 1;
const forceTextSpan = fragment.firstElementChild.dataset.textSpan === "force";
if (hasOnlyOneParagraph
&& hasOnlyOneTextSpan
&& forceTextSpan) {
selectionController.replaceText(fragment.textContent);
} else {
selectionController.replaceWithPaste(fragment);
}
selectionController.replaceWithPaste(fragment);
}
}

View File

@@ -230,19 +230,14 @@ export function mapContentFragmentFromString(string, styleDefaults) {
const fragment = document.createDocumentFragment();
for (const line of lines) {
if (line === "") {
fragment.appendChild(
createEmptyParagraph(styleDefaults)
);
fragment.appendChild(createEmptyParagraph(styleDefaults));
} else {
const textSpan = createTextSpan(new Text(line), styleDefaults);
const paragraph = createParagraph(
[textSpan],
styleDefaults,
fragment.appendChild(
createParagraph(
[createTextSpan(new Text(line), styleDefaults)],
styleDefaults,
),
);
if (lines.length === 1) {
paragraph.dataset.textSpan = "force";
}
fragment.appendChild(paragraph);
}
}
return fragment;

View File

@@ -6,7 +6,6 @@
* Copyright (c) KALEIDOS INC
*/
import StyleDeclaration from '../../controllers/StyleDeclaration.js';
import { getFills } from "./Color.js";
const DEFAULT_FONT_SIZE = "16px";
@@ -339,14 +338,13 @@ export function setStylesFromObject(element, allowedStyles, styleObject) {
continue;
}
let styleValue = styleObject[styleName];
if (!styleValue)
continue;
if (styleName === "font-family") {
styleValue = sanitizeFontFamily(styleValue);
}
setStyle(element, styleName, styleValue, styleUnit);
if (styleValue) {
setStyle(element, styleName, styleValue, styleUnit);
}
}
return element;
}
@@ -388,8 +386,7 @@ export function setStylesFromDeclaration(
* @returns {HTMLElement}
*/
export function setStyles(element, allowedStyles, styleObjectOrDeclaration) {
if (styleObjectOrDeclaration instanceof CSSStyleDeclaration
|| styleObjectOrDeclaration instanceof StyleDeclaration) {
if (styleObjectOrDeclaration instanceof CSSStyleDeclaration) {
return setStylesFromDeclaration(
element,
allowedStyles,
@@ -429,15 +426,13 @@ export function mergeStyles(allowedStyles, styleDeclaration, newStyles) {
const mergedStyles = {};
for (const [styleName, styleUnit] of allowedStyles) {
if (styleName in newStyles) {
const styleValue = newStyles[styleName];
mergedStyles[styleName] = styleValue;
mergedStyles[styleName] = newStyles[styleName];
} else {
const styleValue = getStyleFromDeclaration(
mergedStyles[styleName] = getStyleFromDeclaration(
styleDeclaration,
styleName,
styleUnit,
);
mergedStyles[styleName] = styleValue;
}
}
return mergedStyles;

View File

@@ -6,8 +6,6 @@
* Copyright (c) KALEIDOS INC
*/
import SafeGuard from '../../controllers/SafeGuard.js';
/**
* Iterator direction.
*
@@ -247,51 +245,6 @@ export class TextNodeIterator {
this.#currentNode = previousNode;
return this.#currentNode;
}
/**
* Returns an array of text nodes.
*
* @param {TextNode} startNode
* @param {TextNode} endNode
* @returns {Array<TextNode>}
*/
collectFrom(startNode, endNode) {
const nodes = [];
for (const node of this.iterateFrom(startNode, endNode)) {
nodes.push(node);
}
return nodes;
}
/**
* Iterates over a list of nodes.
*
* @param {TextNode} startNode
* @param {TextNode} endNode
* @yields {TextNode}
*/
* iterateFrom(startNode, endNode) {
const comparedPosition = startNode.compareDocumentPosition(
endNode
);
this.#currentNode = startNode;
SafeGuard.start();
while (this.#currentNode !== endNode) {
yield this.#currentNode;
SafeGuard.update();
if (comparedPosition === Node.DOCUMENT_POSITION_PRECEDING) {
if (!this.previousNode()) {
break;
}
} else if (comparedPosition === Node.DOCUMENT_POSITION_FOLLOWING) {
if (!this.nextNode()) {
break;
}
} else {
break;
}
}
}
}
export default TextNodeIterator;

View File

@@ -28,20 +28,7 @@ export function update() {
}
}
let timeoutId = 0
export function throwAfter(error, timeout = SAFE_GUARD_TIME) {
timeoutId = setTimeout(() => {
throw error
}, timeout)
}
export function throwCancel() {
clearTimeout(timeoutId)
}
export default {
start,
update,
throwAfter,
throwCancel,
};
}

View File

@@ -54,7 +54,6 @@ import { isRoot, setRootStyles } from "../content/dom/Root.js";
import { SelectionDirection } from "./SelectionDirection.js";
import SafeGuard from "./SafeGuard.js";
import { sanitizeFontFamily } from "../content/dom/Style.js";
import StyleDeclaration from './StyleDeclaration.js';
/**
* Supported options for the SelectionController.
@@ -65,7 +64,39 @@ import StyleDeclaration from './StyleDeclaration.js';
/**
* SelectionController uses the same concepts used by the Selection API but extending it to support
* our own internal model based on paragraphs (in draft.js they were called blocks) and text spans.
* our own internal model based on paragraphs (in drafconst textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
createParagraph([createTextSpan(new Text("Hello, "))]),
createEmptyParagraph(),
createParagraph([createTextSpan(new Text("World!"))]),
]);
const root = textEditorMock.root;
const selection = document.getSelection();
const selectionController = new SelectionController(
textEditorMock,
selection
);
focus(
selection,
textEditorMock,
root.childNodes.item(2).firstChild.firstChild,
0
);
selectionController.mergeBackwardParagraph();
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.children.length).toBe(2);
expect(textEditorMock.root.dataset.itype).toBe("root");
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
expect(textEditorMock.root.firstChild.dataset.itype).toBe("paragraph");
expect(textEditorMock.root.firstChild.firstChild).toBeInstanceOf(
HTMLSpanElement
);
expect(textEditorMock.root.firstChild.firstChild.dataset.itype).toBe(
"span"
);
expect(textEditorMock.root.textContent).toBe("Hello, World!");
expect(textEditorMock.root.firstChild.textContent).toBe("Hello, ");
expect(textEditorMock.root.lastChild.textContent).toBe("World!");
t.js they were called blocks) and text spans.
*/
export class SelectionController extends EventTarget {
/**
@@ -133,12 +164,21 @@ export class SelectionController extends EventTarget {
#textNodeIterator = null;
/**
* StyleDeclaration that we can mutate
* CSSStyleDeclaration that we can mutate
* to handle style changes.
*
* @type {StyleDeclaration}
* @type {CSSStyleDeclaration}
*/
#currentStyle = new StyleDeclaration();
#currentStyle = null;
/**
* Element used to have a custom CSSStyleDeclaration
* that we can modify to handle style changes when the
* selection is changed.
*
* @type {HTMLDivElement}
*/
#inertElement = null;
/**
* @type {SelectionControllerDebug}
@@ -235,62 +275,19 @@ export class SelectionController extends EventTarget {
*
* @param {HTMLElement} element
*/
#applyStylesFromElementToCurrentStyle(element) {
#applyStylesToCurrentStyle(element) {
for (let index = 0; index < element.style.length; index++) {
const styleName = element.style.item(index);
if (styleName === "--fills") {
continue;
}
let styleValue = element.style.getPropertyValue(styleName);
let styleValue = element.style.getPropertyValue(styleName);
if (styleName === "font-family") {
styleValue = sanitizeFontFamily(styleValue);
}
this.#currentStyle.setProperty(styleName, styleValue);
}
}
/**
* Applies some styles to the currentStyle
* CSSStyleDeclaration
*
* @param {HTMLElement} element
*/
#mergeStylesFromElementToCurrentStyle(element) {
for (let index = 0; index < element.style.length; index++) {
const styleName = element.style.item(index);
let styleValue = element.style.getPropertyValue(styleName);
if (styleName === "font-family") {
styleValue = sanitizeFontFamily(styleValue);
}
this.#currentStyle.mergeProperty(styleName, styleValue);
}
}
/**
* Updates current styles based on the currently selected text spans.
*
* @param {HTMLSpanElement} startNode
* @param {HTMLSpanElement} endNode
*/
#updateCurrentStyleFrom(startNode, endNode) {
this.#applyDefaultStylesToCurrentStyle();
const root = startNode.parentElement.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(root);
// FIXME: I don't like this approximation. Having to iterate nodes twice
// is bad for performance. I think we need another way of "computing"
// the cascade.
for (const textNode of this.#textNodeIterator.iterateFrom(startNode, endNode)) {
const paragraph = textNode.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(paragraph);
}
for (const textNode of this.#textNodeIterator.iterateFrom(startNode, endNode)) {
const textSpan = textNode.parentElement;
this.#mergeStylesFromElementToCurrentStyle(textSpan);
}
return this;
}
/**
* Updates current styles based on the currently selected text span.
*
@@ -300,14 +297,20 @@ export class SelectionController extends EventTarget {
#updateCurrentStyle(textSpan) {
this.#applyDefaultStylesToCurrentStyle();
const root = textSpan.parentElement.parentElement;
this.#applyStylesFromElementToCurrentStyle(root);
this.#applyStylesToCurrentStyle(root);
const paragraph = textSpan.parentElement;
this.#applyStylesFromElementToCurrentStyle(paragraph);
this.#applyStylesFromElementToCurrentStyle(textSpan);
this.#applyStylesToCurrentStyle(paragraph);
this.#applyStylesToCurrentStyle(textSpan);
return this;
}
#updateState() {
/**
* This is called on every `selectionchange` because it is dispatched
* only by the `document` object.
*
* @param {Event} e
*/
#onSelectionChange = (e) => {
// If we're outside the contenteditable element, then
// we return.
if (!this.hasFocus) {
@@ -317,23 +320,17 @@ export class SelectionController extends EventTarget {
let focusNodeChanges = false;
let anchorNodeChanges = false;
if (
this.#focusNode !== this.#selection.focusNode ||
this.#focusOffset !== this.#selection.focusOffset
) {
if (this.#focusNode !== this.#selection.focusNode) {
this.#focusNode = this.#selection.focusNode;
this.#focusOffset = this.#selection.focusOffset;
focusNodeChanges = true;
}
this.#focusOffset = this.#selection.focusOffset;
if (
this.#anchorNode !== this.#selection.anchorNode ||
this.#anchorOffset !== this.#selection.anchorOffset
) {
if (this.#anchorNode !== this.#selection.anchorNode) {
this.#anchorNode = this.#selection.anchorNode;
this.#anchorOffset = this.#selection.anchorOffset;
anchorNodeChanges = true;
}
this.#anchorOffset = this.#selection.anchorOffset;
// We need to handle multi selection from firefox
// and remove all the old ranges and just keep the
@@ -362,7 +359,7 @@ export class SelectionController extends EventTarget {
// If focus node changed, we need to retrieve all the
// styles of the current text span and dispatch an event
// to notify that the styles have changed.
if (focusNodeChanges || anchorNodeChanges) {
if (focusNodeChanges) {
this.#notifyStyleChange();
}
@@ -375,42 +372,43 @@ export class SelectionController extends EventTarget {
if (this.#debug) {
this.#debug.update(this);
}
}
/**
* This is called on every `selectionchange` because it is dispatched
* only by the `document` object.
*
* @param {Event} e
*/
#onSelectionChange = (_) => this.#updateState();
};
/**
* Notifies that the styles have changed.
*/
#notifyStyleChange() {
if (this.#selection.isCollapsed) {
// CARET
const textSpan =
this.focusTextSpan ??
this.#textEditor.root?.firstElementChild?.firstElementChild;
const textSpan = this.focusTextSpan;
if (textSpan) {
this.#updateCurrentStyle(textSpan);
this.dispatchEvent(
new CustomEvent("stylechange", {
detail: this.#currentStyle,
}),
);
} else {
// SELECTION.
this.#updateCurrentStyleFrom(this.#anchorNode, this.#focusNode);
const firstTextSpan =
this.#textEditor.root?.firstElementChild?.firstElementChild;
if (firstTextSpan) {
this.#updateCurrentStyle(firstTextSpan);
this.dispatchEvent(
new CustomEvent("stylechange", {
detail: this.#currentStyle,
}),
);
}
}
this.dispatchEvent(
new CustomEvent("stylechange", {
detail: this.#currentStyle,
}),
);
}
/**
* Setups
*/
#setup() {
// This element is not attached to the DOM
// so it doesn't trigger style or layout calculations.
// That's why it's called "inertElement".
this.#inertElement = document.createElement("div");
this.#currentStyle = this.#inertElement.style;
this.#applyDefaultStylesToCurrentStyle();
if (this.#selection.rangeCount > 0) {
@@ -430,22 +428,6 @@ export class SelectionController extends EventTarget {
document.addEventListener("selectionchange", this.#onSelectionChange);
}
/**
* Disposes the current resources.
*/
dispose() {
document.removeEventListener("selectionchange", this.#onSelectionChange);
this.#textEditor = null;
this.#ranges.clear();
this.#ranges = null;
this.#range = null;
this.#selection = null;
this.#focusNode = null;
this.#anchorNode = null;
this.#mutations.dispose();
this.#mutations = null;
}
/**
* Returns a Range-like object.
*
@@ -520,8 +502,6 @@ export class SelectionController extends EventTarget {
* Marks the start of a mutation.
*
* Clears all the mutations kept in CommandMutations.
*
* @returns {boolean}
*/
startMutation() {
this.#mutations.clear();
@@ -532,7 +512,7 @@ export class SelectionController extends EventTarget {
/**
* Marks the end of a mutation.
*
* @returns {CommandMutations}
* @returns
*/
endMutation() {
return this.#mutations;
@@ -540,8 +520,6 @@ export class SelectionController extends EventTarget {
/**
* Selects all content.
*
* @returns {SelectionController}
*/
selectAll() {
if (this.#textEditor.isEmpty) {
@@ -580,15 +558,23 @@ export class SelectionController extends EventTarget {
this.#selection.removeAllRanges();
this.#selection.addRange(range);
this.#updateState();
// Ensure internal state is synchronized
this.#focusNode = this.#selection.focusNode;
this.#focusOffset = this.#selection.focusOffset;
this.#anchorNode = this.#selection.anchorNode;
this.#anchorOffset = this.#selection.anchorOffset;
this.#range = range;
this.#ranges.clear();
this.#ranges.add(range);
// Notify style changes
this.#notifyStyleChange();
return this;
}
/**
* Moves cursor to end.
*
* @returns {SelectionController}
*/
cursorToEnd() {
const range = document.createRange(); //Create a range (a range is a like the selection but invisible)
@@ -676,6 +662,22 @@ export class SelectionController extends EventTarget {
}
}
/**
* Disposes the current resources.
*/
dispose() {
document.removeEventListener("selectionchange", this.#onSelectionChange);
this.#textEditor = null;
this.#ranges.clear();
this.#ranges = null;
this.#range = null;
this.#selection = null;
this.#focusNode = null;
this.#anchorNode = null;
this.#mutations.dispose();
this.#mutations = null;
}
/**
* Returns the current selection.
*
@@ -1112,8 +1114,8 @@ export class SelectionController extends EventTarget {
return isParagraphEnd(this.focusNode, this.focusOffset);
}
#getFragmentTextSpanTextNode(fragment) {
if (isTextSpan(fragment.firstElementChild.lastChild)) {
#getFragmentInlineTextNode(fragment) {
if (isInline(fragment.firstElementChild.lastChild)) {
return fragment.firstElementChild.firstElementChild.lastChild;
}
return fragment.firstElementChild.lastChild;
@@ -1129,15 +1131,11 @@ export class SelectionController extends EventTarget {
* @param {DocumentFragment} fragment
*/
insertPaste(fragment) {
const hasOnlyOneParagraph = fragment.children.length === 1;
const forceTextSpan =
fragment.firstElementChild?.dataset?.textSpan === "force";
if (
hasOnlyOneParagraph &&
forceTextSpan
fragment.children.length === 1 &&
fragment.firstElementChild?.dataset?.textSpan === "force"
) {
// first text span
const collapseNode = fragment.firstElementChild.firstElementChild;
const collapseNode = fragment.firstElementChild.firstChild;
if (this.isTextSpanStart) {
this.focusTextSpan.before(...fragment.firstElementChild.children);
} else if (this.isTextSpanEnd) {
@@ -1149,9 +1147,7 @@ export class SelectionController extends EventTarget {
newTextSpan,
);
}
// collapseNode could be a <br>, that's why we need to
// make `nodeValue` as optional.
return this.collapse(collapseNode, collapseNode?.nodeValue?.length || 0);
return this.collapse(collapseNode, collapseNode.nodeValue?.length || 0);
}
const collapseNode = this.#getFragmentParagraphTextNode(fragment);
if (this.isParagraphStart) {
@@ -1397,16 +1393,9 @@ export class SelectionController extends EventTarget {
this.focusOffset,
newText,
);
this.collapse(this.focusNode, this.focusOffset + newText.length);
} else if (this.isLineBreakFocus) {
const textNode = new Text(newText);
// the focus node is a <span>.
if (isTextSpan(this.focusNode)) {
this.focusNode.firstElementChild.replaceWith(textNode);
// the focus node is a <br>.
} else {
this.focusNode.replaceWith(textNode);
}
this.focusNode.replaceWith(textNode);
this.collapse(textNode, newText.length);
} else {
throw new Error("Unknown node type");
@@ -1943,21 +1932,11 @@ export class SelectionController extends EventTarget {
const textSpan = this.startTextSpan;
const midText = startNode.splitText(startOffset);
const endText = midText.splitText(endOffset - startOffset);
// Only create text span if midText is not empty
if (midText.nodeValue && midText.nodeValue.length > 0) {
const midTextSpan = createTextSpanFrom(textSpan, midText, newStyles);
textSpan.after(midTextSpan);
if (endText.length > 0) {
const endTextSpan = createTextSpan(endText, textSpan.style);
midTextSpan.after(endTextSpan);
}
} else {
// If midText is empty, just create endTextSpan if needed
if (endText.length > 0) {
const endTextSpan = createTextSpan(endText, textSpan.style);
textSpan.after(endTextSpan);
}
const midTextSpan = createTextSpanFrom(textSpan, midText, newStyles);
textSpan.after(midTextSpan);
if (endText.length > 0) {
const endTextSpan = createTextSpan(endText, textSpan.style);
midTextSpan.after(endTextSpan);
}
// NOTE: This is necessary because sometimes
@@ -1974,21 +1953,16 @@ export class SelectionController extends EventTarget {
// the styles are applied to the current caret
else if (
this.startOffset === this.endOffset &&
this.endOffset === endNode.nodeValue?.length
this.endOffset === endNode.nodeValue.length
) {
const newTextSpan = createVoidTextSpan(newStyles);
this.endTextSpan.after(newTextSpan);
this.setSelection(newTextSpan.firstChild, 0, newTextSpan.firstChild, 0);
}
// The styles are applied to the paragraph
else
{
else {
const paragraph = this.startParagraph;
setParagraphStyles(paragraph, newStyles);
// Apply styles to child text spans.
for (const textSpan of paragraph.children) {
setTextSpanStyles(textSpan, newStyles);
}
}
return this.#notifyStyleChange();
@@ -2010,8 +1984,7 @@ export class SelectionController extends EventTarget {
// new text span.
if (
this.#textNodeIterator.currentNode === startNode &&
startOffset > 0 &&
startOffset < (startNode.nodeValue?.length || 0)
startOffset > 0
) {
const newTextSpan = splitTextSpan(textSpan, startOffset);
setTextSpanStyles(newTextSpan, newStyles);
@@ -2026,15 +1999,14 @@ export class SelectionController extends EventTarget {
(this.#textNodeIterator.currentNode !== startNode &&
this.#textNodeIterator.currentNode !== endNode) ||
(this.#textNodeIterator.currentNode === endNode &&
endOffset === endNode.nodeValue?.length)
endOffset === endNode.nodeValue.length)
) {
setTextSpanStyles(textSpan, newStyles);
// If we're at end node
} else if (
this.#textNodeIterator.currentNode === endNode &&
endOffset < endNode.nodeValue?.length &&
endOffset > 0
endOffset < endNode.nodeValue.length
) {
const newTextSpan = splitTextSpan(textSpan, endOffset);
setTextSpanStyles(textSpan, newStyles);

View File

@@ -1,110 +0,0 @@
export class StyleDeclaration {
static Property = class Property {
static NULL = '["~#\'",null]';
static Default = new Property("", "", "");
name;
value = "";
priority = "";
constructor(name, value = "", priority = "") {
this.name = name;
this.value = value ?? "";
this.priority = priority ?? "";
}
};
#items = new Map();
get cssFloat() {
throw new Error("Not implemented");
}
get cssText() {
throw new Error("Not implemented");
}
get parentRule() {
throw new Error("Not implemented");
}
get length() {
return this.#items.size;
}
#getProperty(name) {
return this.#items.get(name) ?? StyleDeclaration.Property.Default;
}
getPropertyPriority(name) {
const { priority } = this.#getProperty(name);
return priority ?? "";
}
getPropertyValue(name) {
const { value } = this.#getProperty(name);
return value ?? "";
}
item(index) {
return Array.from(this.#items).at(index).name;
}
removeProperty(name) {
const value = this.getPropertyValue(name);
this.#items.delete(name);
return value;
}
setProperty(name, value, priority) {
this.#items.set(name, new StyleDeclaration.Property(name, value, priority));
}
/** Non compatible methods */
#isQuotedValue(a, b) {
if (a.startsWith('"') && b.startsWith('"')) {
return a === b;
} else if (a.startsWith('"') && !b.startsWith('"')) {
return a.slice(1, -1) === b;
} else if (!a.startsWith('"') && b.startsWith('"')) {
return a === b.slice(1, -1);
}
return a === b;
}
mergeProperty(name, value) {
const currentValue = this.getPropertyValue(name);
if (this.#isQuotedValue(currentValue, value)) {
return this.setProperty(name, value);
} else if (currentValue === "" && value === StyleDeclaration.Property.NULL) {
return this.setProperty(name, value);
} else if (currentValue === "" && ["initial", "none"].includes(value)) {
return this.setProperty(name, value);
} else if (currentValue !== value && name === "--fills") {
return this.setProperty(name, value);
} else if (currentValue !== value) {
return this.setProperty(name, "mixed");
}
}
fromCSSStyleDeclaration(cssStyleDeclaration) {
for (let index = 0; index < cssStyleDeclaration.length; index++) {
const name = cssStyleDeclaration.item(index);
const value = cssStyleDeclaration.getPropertyValue(name);
const priority = cssStyleDeclaration.getPropertyPriority(name);
this.setProperty(name, value, priority);
}
}
toObject() {
return Object.fromEntries(
Array.from(this.#items.entries(), ([name, property]) => [
name,
property.value,
]),
);
}
}
export default StyleDeclaration

View File

@@ -1,32 +0,0 @@
import { describe, test, expect } from "vitest";
import { StyleDeclaration } from "./StyleDeclaration.js";
describe("StyleDeclaration", () => {
test("Create a new StyleDeclaration", () => {
const styleDeclaration = new StyleDeclaration();
expect(styleDeclaration).toBeInstanceOf(StyleDeclaration);
});
test("Uninmplemented getters should throw", () => {
expect(() => styleDeclaration.cssFloat).toThrow();
expect(() => styleDeclaration.cssText).toThrow();
expect(() => styleDeclaration.parentRule).toThrow();
});
test("Set property", () => {
const styleDeclaration = new StyleDeclaration();
styleDeclaration.setProperty("line-height", "1.2");
expect(styleDeclaration.getPropertyValue("line-height")).toBe("1.2");
expect(styleDeclaration.getPropertyPriority("line-height")).toBe("");
});
test("Remove property", () => {
const styleDeclaration = new StyleDeclaration();
styleDeclaration.setProperty("line-height", "1.2");
expect(styleDeclaration.getPropertyValue("line-height")).toBe("1.2");
expect(styleDeclaration.getPropertyPriority("line-height")).toBe("");
styleDeclaration.removeProperty("line-height");
expect(styleDeclaration.getPropertyValue("line-height")).toBe("");
expect(styleDeclaration.getPropertyPriority("line-height")).toBe("");
});
});

View File

@@ -7745,6 +7745,10 @@ msgstr ""
"Invalid token value: only none, Uppercase, Lowercase or Capitalize are "
"accepted"
#: src/app/main/data/workspace/tokens/errors.cljs:93
msgid "workspace.tokens.invalid-font-family-token-value"
msgstr "Invalid token value: you can only reference a font-family token"
#: src/app/main/data/workspace/tokens/errors.cljs:85
msgid "workspace.tokens.invalid-text-decoration-token-value"
msgstr "Invalid token value: only none, underline and strike-through are accepted"

View File

@@ -7670,6 +7670,14 @@ msgstr ""
msgid "workspace.tokens.invalid-shadow-type-token-value"
msgstr "Tipo de sombra no válida: solo se aceptan 'innerShadow' o 'dropShadow'"
#: src/app/main/data/workspace/tokens/errors.cljs:93
msgid "workspace.tokens.invalid-font-family-token-value"
msgstr "Valor de token no válido: solo puedes referenciar tokens tipo font-family"
#: src/app/main/data/workspace/tokens/errors.cljs:85
msgid "workspace.tokens.invalid-text-decoration-token-value"
msgstr "Valor de token no válido: solo none, underline y strike-through son aceptados"
#: src/app/main/data/workspace/tokens/errors.cljs:93
msgid "workspace.tokens.invalid-token-value-typography"
msgstr "Valor no válido: debe hacer referencia a un token tipográfico compuesto."

View File

@@ -1226,16 +1226,16 @@ __metadata:
languageName: node
linkType: hard
"@penpot/svgo@penpot/svgo#v3.1":
"@penpot/svgo@penpot/svgo#v3.2":
version: 4.0.0
resolution: "@penpot/svgo@https://github.com/penpot/svgo.git#commit=a46262c12c0d967708395972c374eb2adead4180"
resolution: "@penpot/svgo@https://github.com/penpot/svgo.git#commit=8c9b0e32e9cb5f106085260bd9375f3c91a5010b"
dependencies:
"@trysound/sax": "npm:0.2.0"
css-select: "npm:^5.1.0"
css-tree: "npm:^3.1.0"
csso: "npm:^5.0.5"
lodash: "npm:^4.17.21"
checksum: 10c0/db5f81c99dec2765721d73b69bb30594869ebf657380dfb46709c79775b6c0dc1af678fe9fe51bbe2272a2c78d19c2694a12ec6578bcc41235fa4aff475c9416
checksum: 10c0/d7af2801451b97f8ffb17664147c609456f5bcc786c6d03b222546125260c0f268e750748311d61598e31f66610b00038d2b969635b1a15e5694647e19c6b63a
languageName: node
linkType: hard
@@ -4311,7 +4311,7 @@ __metadata:
"@penpot/hljs": "portal:./vendor/hljs"
"@penpot/mousetrap": "portal:./vendor/mousetrap"
"@penpot/plugins-runtime": "npm:1.3.2"
"@penpot/svgo": "penpot/svgo#v3.1"
"@penpot/svgo": "penpot/svgo#v3.2"
"@penpot/text-editor": "portal:./text-editor"
"@playwright/test": "npm:1.52.0"
"@storybook/addon-docs": "npm:10.0.4"

View File

@@ -4,7 +4,7 @@
"license": "MPL-2.0",
"author": "Kaleidos INC",
"private": true,
"packageManager": "yarn@4.9.1+sha512.f95ce356460e05be48d66401c1ae64ef84d163dd689964962c6888a9810865e39097a5e9de748876c2e0bf89b232d583c33982773e9903ae7a76257270986538",
"packageManager": "yarn@4.12.0+sha512.f45ab632439a67f8bc759bf32ead036a1f413287b9042726b7cc4818b7b49e14e9423ba49b18f9e06ea4941c1ad062385b1d8760a8d5091a1a31e5f6219afca8",
"repository": {
"type": "git",
"url": "https://github.com/penpot/penpot"
@@ -15,6 +15,7 @@
"fmt": "./scripts/fmt"
},
"devDependencies": {
"@types/node": "^20.12.7"
"@types/node": "^20.12.7",
"esbuild": "^0.25.9"
}
}

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