mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 15:51:32 -05:00
Compare commits
9 Commits
alotor-plu
...
eva-fix-er
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b867e08b00 | ||
|
|
9ca76c745f | ||
|
|
3112b240a0 | ||
|
|
56fd66b91a | ||
|
|
1ce0b60e3d | ||
|
|
ef80901400 | ||
|
|
5306bed548 | ||
|
|
92a319ddd1 | ||
|
|
68a6d4c9a8 |
16
.github/workflows/plugins-deploy-api-doc.yml
vendored
16
.github/workflows/plugins-deploy-api-doc.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- "plugins/libs/plugin-types/REAME.md"
|
||||
- "plugins/tools/typedoc.css"
|
||||
- "plugins/CHANGELOG.md"
|
||||
- "plugins/wrangle-penpot-plugins-api-doc.toml"
|
||||
- "plugins/wrangler-penpot-plugins-api-doc.toml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
@@ -98,4 +98,16 @@ jobs:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
command: deploy --config wrangler-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📚 *[PENPOT PLUGINS] Error deploying API documentation.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Plugins/package deployer
|
||||
|
||||
on:
|
||||
# Deploy package from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
plugin_name:
|
||||
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
plugin_name:
|
||||
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: penpot-runner-01
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
# START: Setup Node and PNPM enabling cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable PNPM
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
# END: Setup Node and PNPM enabling cache
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile;
|
||||
pnpm add -D -w wrangler@latest;
|
||||
|
||||
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: npx nx build ${{ inputs.plugin_name }}-plugin
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ inputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||
staging)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||
develop)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Set the custom url
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
Plugin name: `${{ inputs.plugin_name }}-plugin`
|
||||
Cloudflare worker name: `${{ env.WORKER_NAME }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Plugins/packages deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/apps/*-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
|
||||
create_palette: ${{ steps.filter.outputs.create_palette }}
|
||||
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
|
||||
rename_layers: ${{ steps.filter.outputs.rename_layers }}
|
||||
contrast: ${{ steps.filter.outputs.contrast }}
|
||||
icons: ${{ steps.filter.outputs.icons }}
|
||||
poc_state: ${{ steps.filter.outputs.poc_state }}
|
||||
table: ${{ steps.filter.outputs.table }}
|
||||
# [For new plugins]
|
||||
# Add more outputs here
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
colors_to_tokens:
|
||||
- 'plugins/apps/colors-to-tokens-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
contrast:
|
||||
- 'plugins/apps/contrast-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
create_palette:
|
||||
- 'plugins/apps/create-palette-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
icons:
|
||||
- 'plugins/apps/icons-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
lorem_ipsum:
|
||||
- 'plugins/apps/lorem-ipsum-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
rename_layers:
|
||||
- 'plugins/apps/rename-layers-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
table:
|
||||
- 'plugins/apps/table-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
# [For new plugins]
|
||||
# Add more plugin filters here
|
||||
# another_plugin:
|
||||
# - 'plugins/apps/another-plugin/**'
|
||||
# - 'libs/plugins-styles/**'
|
||||
|
||||
colors-to-tokens-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: colors-to-tokens
|
||||
|
||||
contrast-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: contrast
|
||||
|
||||
create-palette-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: create-palette
|
||||
|
||||
icons-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: icons
|
||||
|
||||
lorem-ipsum-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: lorem-ipsum
|
||||
|
||||
rename-layers-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: rename-layers
|
||||
|
||||
table-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: table
|
||||
|
||||
# [For new plugins]
|
||||
# Add more jobs for other plugins below, following the same pattern
|
||||
# another-plugin:
|
||||
# needs: detect-changes
|
||||
# if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true'
|
||||
# uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
# secrets: inherit
|
||||
# with:
|
||||
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
# plugin_name: another
|
||||
@@ -29,7 +29,6 @@
|
||||
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
|
||||
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
|
||||
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
|
||||
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
|
||||
- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167)
|
||||
- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171)
|
||||
@@ -37,7 +36,9 @@
|
||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
|
||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||
|
||||
- Fix several race conditions on path editor [Github #8187](https://github.com/penpot/penpot/pull/8187)
|
||||
- Fix app freeze when introducing an error on a very long token name [Taiga #13214](https://tree.taiga.io/project/penpot/issue/13214)
|
||||
- Fix error when creating a token with an invalid name [Taiga #13219](https://tree.taiga.io/project/penpot/issue/13219)
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
@@ -526,25 +526,20 @@
|
||||
ids))
|
||||
|
||||
(defn clean-loops
|
||||
"Clean a list of ids from circular references. Optimized fast-path for single selections."
|
||||
"Clean a list of ids from circular references."
|
||||
[objects ids]
|
||||
(if (<= (count ids) 1)
|
||||
;; For single selection, there can't be circularity; return as ordered-set.
|
||||
(into (d/ordered-set) ids)
|
||||
(let [ids-set (if (set? ids) ids (set ids))
|
||||
parent-selected?
|
||||
(fn [id]
|
||||
;; Stop early as soon as we find any selected parent
|
||||
(let [parents (get-parent-ids objects id)]
|
||||
(some #(contains? ids-set %) parents)))
|
||||
(let [parent-selected?
|
||||
(fn [id]
|
||||
(let [parents (get-parent-ids objects id)]
|
||||
(some ids parents)))
|
||||
|
||||
add-element
|
||||
(fn [result id]
|
||||
(cond-> result
|
||||
(not (parent-selected? id))
|
||||
(conj id)))]
|
||||
add-element
|
||||
(fn [result id]
|
||||
(cond-> result
|
||||
(not (parent-selected? id))
|
||||
(conj id)))]
|
||||
|
||||
(reduce add-element (d/ordered-set) ids))))
|
||||
(reduce add-element (d/ordered-set) ids)))
|
||||
|
||||
(defn- indexed-shapes
|
||||
"Retrieves a vector with the indexes for each element in the layer
|
||||
|
||||
@@ -134,8 +134,6 @@
|
||||
:subscriptions
|
||||
:subscriptions-old
|
||||
:inspect-styles
|
||||
;; Enable performance logs in devconsole (disabled by default)
|
||||
:perf-logs
|
||||
|
||||
;; Security layer middleware that filters request by fetch
|
||||
;; metadata headers
|
||||
|
||||
@@ -124,51 +124,33 @@
|
||||
|
||||
(defn adjust-to-viewport
|
||||
([viewport srect] (adjust-to-viewport viewport srect nil))
|
||||
([viewport srect {:keys [padding min-zoom] :or {padding 0 min-zoom nil}}]
|
||||
([viewport srect {:keys [padding] :or {padding 0}}]
|
||||
(let [gprop (/ (:width viewport)
|
||||
(:height viewport))
|
||||
srect-padded (-> srect
|
||||
(update :x #(- % padding))
|
||||
(update :y #(- % padding))
|
||||
(update :width #(+ % padding padding))
|
||||
(update :height #(+ % padding padding)))
|
||||
width (:width srect-padded)
|
||||
height (:height srect-padded)
|
||||
lprop (/ width height)
|
||||
adjusted-rect
|
||||
(cond
|
||||
(> gprop lprop)
|
||||
(let [width' (* (/ width lprop) gprop)
|
||||
padding (/ (- width' width) 2)]
|
||||
(-> srect-padded
|
||||
(update :x #(- % padding))
|
||||
(assoc :width width')
|
||||
(grc/update-rect :position)))
|
||||
|
||||
(< gprop lprop)
|
||||
(let [height' (/ (* height lprop) gprop)
|
||||
padding (/ (- height' height) 2)]
|
||||
(-> srect-padded
|
||||
(update :y #(- % padding))
|
||||
(assoc :height height')
|
||||
(grc/update-rect :position)))
|
||||
|
||||
:else
|
||||
(grc/update-rect srect-padded :position))]
|
||||
;; If min-zoom is specified and the resulting zoom would be below it,
|
||||
;; return a rect with the original top-left corner centered in the viewport
|
||||
;; instead of using the aspect-ratio-adjusted rect (which can push coords
|
||||
;; extremely far with extreme aspect ratios).
|
||||
(if (and (some? min-zoom)
|
||||
(< (/ (:width viewport) (:width adjusted-rect)) min-zoom))
|
||||
(let [anchor-x (:x srect)
|
||||
anchor-y (:y srect)
|
||||
vbox-width (/ (:width viewport) min-zoom)
|
||||
vbox-height (/ (:height viewport) min-zoom)]
|
||||
(-> adjusted-rect
|
||||
(assoc :x (- anchor-x (/ vbox-width 2))
|
||||
:y (- anchor-y (/ vbox-height 2))
|
||||
:width vbox-width
|
||||
:height vbox-height)
|
||||
srect (-> srect
|
||||
(update :x #(- % padding))
|
||||
(update :y #(- % padding))
|
||||
(update :width #(+ % padding padding))
|
||||
(update :height #(+ % padding padding)))
|
||||
width (:width srect)
|
||||
height (:height srect)
|
||||
lprop (/ width height)]
|
||||
(cond
|
||||
(> gprop lprop)
|
||||
(let [width' (* (/ width lprop) gprop)
|
||||
padding (/ (- width' width) 2)]
|
||||
(-> srect
|
||||
(update :x #(- % padding))
|
||||
(assoc :width width')
|
||||
(grc/update-rect :position)))
|
||||
adjusted-rect))))
|
||||
|
||||
(< gprop lprop)
|
||||
(let [height' (/ (* height lprop) gprop)
|
||||
padding (/ (- height' height) 2)]
|
||||
(-> srect
|
||||
(update :y #(- % padding))
|
||||
(assoc :height height')
|
||||
(grc/update-rect :position)))
|
||||
|
||||
:else
|
||||
(grc/update-rect srect :position)))))
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
|
||||
(def token-name-ref
|
||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
||||
#"^(?!\$)([a-zA-Z0-9-$_]+\.?)*(?<!\.)$"])
|
||||
#"^[a-zA-Z0-9_-][a-zA-Z0-9$_-]*(\.[a-zA-Z0-9$_-]+)*$"])
|
||||
|
||||
(def ^:private schema:color
|
||||
[:map
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"layout/grid",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~ud7430f09-4f59-8049-8007-6277bb7586f6",
|
||||
"~: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": "flex_index_position",
|
||||
"~:revn": 114,
|
||||
"~:modified-at": "~m1769430362161",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u31fe2e21-73e7-80f3-8007-73894fb58240",
|
||||
"~:is-shared": false,
|
||||
"~:migrations": {
|
||||
"~#ordered-set": [
|
||||
"legacy-2",
|
||||
"legacy-3",
|
||||
"legacy-5",
|
||||
"legacy-6",
|
||||
"legacy-7",
|
||||
"legacy-8",
|
||||
"legacy-9",
|
||||
"legacy-10",
|
||||
"legacy-11",
|
||||
"legacy-12",
|
||||
"legacy-13",
|
||||
"legacy-14",
|
||||
"legacy-16",
|
||||
"legacy-17",
|
||||
"legacy-18",
|
||||
"legacy-19",
|
||||
"legacy-25",
|
||||
"legacy-26",
|
||||
"legacy-27",
|
||||
"legacy-28",
|
||||
"legacy-29",
|
||||
"legacy-31",
|
||||
"legacy-32",
|
||||
"legacy-33",
|
||||
"legacy-34",
|
||||
"legacy-36",
|
||||
"legacy-37",
|
||||
"legacy-38",
|
||||
"legacy-39",
|
||||
"legacy-40",
|
||||
"legacy-41",
|
||||
"legacy-42",
|
||||
"legacy-43",
|
||||
"legacy-44",
|
||||
"legacy-45",
|
||||
"legacy-46",
|
||||
"legacy-47",
|
||||
"legacy-48",
|
||||
"legacy-49",
|
||||
"legacy-50",
|
||||
"legacy-51",
|
||||
"legacy-52",
|
||||
"legacy-53",
|
||||
"legacy-54",
|
||||
"legacy-55",
|
||||
"legacy-56",
|
||||
"legacy-57",
|
||||
"legacy-59",
|
||||
"legacy-62",
|
||||
"legacy-65",
|
||||
"legacy-66",
|
||||
"legacy-67",
|
||||
"0001-remove-tokens-from-groups",
|
||||
"0002-normalize-bool-content-v2",
|
||||
"0002-clean-shape-interactions",
|
||||
"0003-fix-root-shape",
|
||||
"0003-convert-path-content-v2",
|
||||
"0005-deprecate-image-type",
|
||||
"0006-fix-old-texts-fills",
|
||||
"0008-fix-library-colors-v4",
|
||||
"0009-clean-library-colors",
|
||||
"0009-add-partial-text-touched-flags",
|
||||
"0010-fix-swap-slots-pointing-non-existent-shapes",
|
||||
"0011-fix-invalid-text-touched-flags",
|
||||
"0012-fix-position-data",
|
||||
"0013-fix-component-path",
|
||||
"0013-clear-invalid-strokes-and-fills",
|
||||
"0014-fix-tokens-lib-duplicate-ids",
|
||||
"0014-clear-components-nil-objects",
|
||||
"0015-fix-text-attrs-blank-strings",
|
||||
"0015-clean-shadow-color",
|
||||
"0016-copy-fills-from-position-data-to-text-node"
|
||||
]
|
||||
},
|
||||
"~:version": 67,
|
||||
"~:project-id": "~ud7430f09-4f59-8049-8007-6277bb765abd",
|
||||
"~:created-at": "~m1769007798998",
|
||||
"~:backend": "legacy-db",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u02e9633d-4ce7-80da-8007-736558496fa8"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u02e9633d-4ce7-80da-8007-736558496fa8": {
|
||||
"~:id": "~u02e9633d-4ce7-80da-8007-736558496fa8",
|
||||
"~:name": "Page 1",
|
||||
"~:objects": {
|
||||
"~#penpot/objects-map/v2": {
|
||||
"~u00000000-0000-0000-0000-000000000000": "[\"~#shape\",[\"^ \",\"~:y\",0,\"~:hide-fill-on-export\",false,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:name\",\"Root Frame\",\"~:width\",0.01,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",0.0,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.0]],[\"^:\",[\"^ \",\"~:x\",0.01,\"~:y\",0.01]],[\"^:\",[\"^ \",\"~:x\",0.0,\"~:y\",0.01]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",0,\"~:proportion\",1.0,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",0,\"~:y\",0,\"^6\",0.01,\"~:height\",0.01,\"~:x1\",0,\"~:y1\",0,\"~:x2\",0.01,\"~:y2\",0.01]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^I\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d50980078e\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc2f\",\"~u94eaebe4-addd-80d1-8007-79d5055d6859\",\"~u77c71dba-32ee-804c-8007-736561cf857f\"]]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cff457": "[\"~#shape\",[\"^ \",\"~:y\",396.00000357564704,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",80,\"~:transforming\",false,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",396.00000357564704]],[\"^>\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",396.00000357564704]],[\"^>\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",476.00000357564704]],[\"^>\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",476.00000357564704]]],\"~:r2\",8,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cff457\",\"~:parent-id\",\"~u77c71dba-32ee-804c-8007-736561cf8584\",\"~:frame-id\",\"~u77c71dba-32ee-804c-8007-736561cf8584\",\"~:strokes\",[],\"~:x\",688.9999775886536,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",396.00000357564704,\"^9\",80,\"~:height\",80,\"~:x1\",688.9999775886536,\"~:y1\",396.00000357564704,\"~:x2\",768.9999775886536,\"~:y2\",476.00000357564704]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e8e9ea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^M\",80,\"~:flip-y\",null]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508aa2885": "[\"~#shape\",[\"^ \",\"~:y\",612.0000188344361,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",80,\"~:transforming\",false,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",604.9999165534973,\"~:y\",612.0000188344361]],[\"^>\",[\"^ \",\"~:x\",684.9999165534973,\"~:y\",612.0000188344361]],[\"^>\",[\"^ \",\"~:x\",684.9999165534973,\"~:y\",692.0000188344361]],[\"^>\",[\"^ \",\"~:x\",604.9999165534973,\"~:y\",692.0000188344361]]],\"~:r2\",8,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2885\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\",\"~:strokes\",[],\"~:x\",604.9999165534973,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",604.9999165534973,\"~:y\",612.0000188344361,\"^9\",80,\"~:height\",80,\"~:x1\",604.9999165534973,\"~:y1\",612.0000188344361,\"~:x2\",684.9999165534973,\"~:y2\",692.0000188344361]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e8e9ea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^M\",80,\"~:flip-y\",null]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508aa2886": "[\"~#shape\",[\"^ \",\"~:y\",636.0000188344361,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:rx\",8,\"~:layout-padding\",[\"^ \",\"~:p1\",8,\"~:p2\",12,\"~:p3\",8,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Dark / Button / Primary / Text / Default\",\"~:layout-align-items\",\"~:center\",\"~:width\",66,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",611.9999165534973,\"~:y\",636.0000188344361]],[\"^K\",[\"^ \",\"~:x\",677.9999165534973,\"~:y\",636.0000188344361]],[\"^K\",[\"^ \",\"~:x\",677.9999165534973,\"~:y\",668.0000188344361]],[\"^K\",[\"^ \",\"~:x\",611.9999165534973,\"~:y\",668.0000188344361]]],\"~:r2\",8,\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^;\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:layout-justify-content\",\"^D\",\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2886\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\",\"~:strokes\",[],\"~:x\",611.9999165534973,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",611.9999165534973,\"~:y\",636.0000188344361,\"^E\",66,\"~:height\",32,\"~:x1\",611.9999165534973,\"~:y1\",636.0000188344361,\"~:x2\",677.9999165534973,\"~:y2\",668.0000188344361]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#7efff5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^17\",32,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d508aa2887\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508aa2887": "[\"~#shape\",[\"^ \",\"~:y\",644.0000188344361,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",2,\"~:p3\",0,\"~:p4\",2],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"_Utilities / Text / White\",\"~:layout-align-items\",\"~:start\",\"~:width\",42,\"~:layout-padding-type\",\"~:simple\",\"~:transforming\",false,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",623.9999165534973,\"~:y\",644.0000188344361]],[\"^K\",[\"^ \",\"~:x\",665.9999165534973,\"~:y\",644.0000188344361]],[\"^K\",[\"^ \",\"~:x\",665.9999165534973,\"~:y\",660.0000188344361]],[\"^K\",[\"^ \",\"~:x\",623.9999165534973,\"~:y\",660.0000188344361]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",6],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2887\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2886\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2886\",\"~:strokes\",[],\"~:x\",623.9999165534973,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",623.9999165534973,\"~:y\",644.0000188344361,\"^D\",42,\"~:height\",16,\"~:x1\",623.9999165534973,\"~:y1\",644.0000188344361,\"~:x2\",665.9999165534973,\"~:y2\",660.0000188344361]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",16,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d508aa2888\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508aa2888": "[\"~#shape\",[\"^ \",\"~:y\",645.0000188344363,\"~: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\",\"~:index\",null,\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^8\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:path\",\"\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"\",\"^<\",\"normal\",\"~:text-transform\",\"uppercase\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-work-sans\",\"~:font-size\",\"12\",\"~:font-weight\",\"500\",\"~:modified-at\",\"2024-06-04T14:15:09.786Z\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"underline\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-color-ref-file\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"~:fill-opacity\",1,\"~:fill-color-ref-id\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:font-family\",\"Work Sans\",\"~:text\",\"Label\"]],\"^=\",\"uppercase\",\"^>\",\"center\",\"^?\",\"gfont-work-sans\",\"^@\",\"12\",\"^A\",\"500\",\"^8\",\"paragraph\",\"^B\",\"2024-06-04T14:15:09.786Z\",\"^C\",\"500\",\"^D\",\"underline\",\"^E\",\"0\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"^K\",\"Work Sans\"]]]],\"~:vertical-align\",\"center\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^I\",1]]],\"~:hide-in-viewer\",true,\"~:name\",\"Input\",\"~:saved-component-root\",null,\"~:width\",38,\"^8\",\"^L\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",625.9999165534973,\"~:y\",645.0000188344363]],[\"^S\",[\"^ \",\"~:x\",663.9999165534973,\"~:y\",645.0000188344363]],[\"^S\",[\"^ \",\"~:x\",663.9999165534973,\"~:y\",660.0000188344359]],[\"^S\",[\"^ \",\"~:x\",625.9999165534973,\"~:y\",660.0000188344363]]],\"~:layout-item-h-sizing\",\"~:fix\",\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2888\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2887\",\"~:position-data\",[[\"^ \",\"~:y\",659.3400268554688,\"^:\",\"1.2\",\"^<\",\"normal\",\"^=\",\"uppercase\",\"^>\",\"left\",\"^?\",\"sourcesanspro\",\"^@\",\"12\",\"^A\",\"500\",\"~:text-direction\",\"ltr\",\"^Q\",37.94000244140625,\"^C\",\"regular\",\"^D\",\"underline\",\"^E\",\"0\",\"~:x\",626.0299682617188,\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:direction\",\"ltr\",\"^K\",\"Work Sans\",\"~:height\",14.08001708984375,\"^L\",\"Label\"]],\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d508aa2887\",\"~:strokes\",[],\"~:x\",625.9999165534973,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",625.9999165534973,\"~:y\",645.0000188344363,\"^Q\",38,\"^11\",15,\"~:x1\",625.9999165534973,\"~:y1\",645.0000188344363,\"~:x2\",663.9999165534973,\"~:y2\",660.0000188344363]],\"^F\",[],\"~:flip-x\",null,\"^11\",15,\"~:flip-y\",null]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cff45a": "[\"~#shape\",[\"^ \",\"~:y\",429.00000357564727,\"~: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\",\"~:index\",null,\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^8\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:path\",\"\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"\",\"^<\",\"normal\",\"~:text-transform\",\"uppercase\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-work-sans\",\"~:font-size\",\"12\",\"~:font-weight\",\"500\",\"~:modified-at\",\"2024-06-04T14:15:09.786Z\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"underline\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-color-ref-file\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"~:fill-opacity\",1,\"~:fill-color-ref-id\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:font-family\",\"Work Sans\",\"~:text\",\"Label\"]],\"^=\",\"uppercase\",\"^>\",\"center\",\"^?\",\"gfont-work-sans\",\"^@\",\"12\",\"^A\",\"500\",\"^8\",\"paragraph\",\"^B\",\"2024-06-04T14:15:09.786Z\",\"^C\",\"500\",\"^D\",\"underline\",\"^E\",\"0\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"^K\",\"Work Sans\"]]]],\"~:vertical-align\",\"center\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^I\",1]]],\"~:hide-in-viewer\",true,\"~:name\",\"Input\",\"~:saved-component-root\",null,\"~:width\",38,\"^8\",\"^L\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",429.00000357564727]],[\"^S\",[\"^ \",\"~:x\",747.9999775886536,\"~:y\",429.00000357564727]],[\"^S\",[\"^ \",\"~:x\",747.9999775886536,\"~:y\",444.0000035756468]],[\"^S\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",444.00000357564727]]],\"~:layout-item-h-sizing\",\"~:fix\",\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cff45a\",\"~:parent-id\",\"~u77c71dba-32ee-804c-8007-736561cff459\",\"~:position-data\",[[\"^ \",\"~:y\",443.3399963378906,\"^:\",\"1.2\",\"^<\",\"normal\",\"^=\",\"uppercase\",\"^>\",\"left\",\"^?\",\"sourcesanspro\",\"^@\",\"12\",\"^A\",\"500\",\"~:text-direction\",\"ltr\",\"^Q\",37.93994140625,\"^C\",\"regular\",\"^D\",\"underline\",\"^E\",\"0\",\"~:x\",710.030029296875,\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:direction\",\"ltr\",\"^K\",\"Work Sans\",\"~:height\",14.079986572265625,\"^L\",\"Label\"]],\"~:frame-id\",\"~u77c71dba-32ee-804c-8007-736561cff459\",\"~:strokes\",[],\"~:x\",709.9999775886536,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",429.00000357564727,\"^Q\",38,\"^11\",15,\"~:x1\",709.9999775886536,\"~:y1\",429.00000357564727,\"~:x2\",747.9999775886536,\"~:y2\",444.00000357564727]],\"^F\",[],\"~:flip-x\",null,\"^11\",15,\"~:flip-y\",null]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cff459": "[\"~#shape\",[\"^ \",\"~:y\",428.00000357564704,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",2,\"~:p3\",0,\"~:p4\",2],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"_Utilities / Text / White\",\"~:layout-align-items\",\"~:start\",\"~:width\",42,\"~:layout-padding-type\",\"~:simple\",\"~:transforming\",false,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",428.00000357564704]],[\"^K\",[\"^ \",\"~:x\",749.9999775886536,\"~:y\",428.00000357564704]],[\"^K\",[\"^ \",\"~:x\",749.9999775886536,\"~:y\",444.00000357564704]],[\"^K\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",444.00000357564704]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",6],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cff459\",\"~:parent-id\",\"~u77c71dba-32ee-804c-8007-736561cff458\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u77c71dba-32ee-804c-8007-736561cff458\",\"~:strokes\",[],\"~:x\",707.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",428.00000357564704,\"^D\",42,\"~:height\",16,\"~:x1\",707.9999775886536,\"~:y1\",428.00000357564704,\"~:x2\",749.9999775886536,\"~:y2\",444.00000357564704]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",16,\"~:flip-y\",null,\"~:shapes\",[\"~u77c71dba-32ee-804c-8007-736561cff45a\"]]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cff458": "[\"~#shape\",[\"^ \",\"~:y\",420.00000357564704,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:rx\",8,\"~:layout-padding\",[\"^ \",\"~:p1\",8,\"~:p2\",12,\"~:p3\",8,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Dark / Button / Primary / Text / Default\",\"~:layout-align-items\",\"~:center\",\"~:width\",66,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",420.00000357564704]],[\"^K\",[\"^ \",\"~:x\",761.9999775886536,\"~:y\",420.00000357564704]],[\"^K\",[\"^ \",\"~:x\",761.9999775886536,\"~:y\",452.00000357564704]],[\"^K\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",452.00000357564704]]],\"~:r2\",8,\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^;\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:layout-justify-content\",\"^D\",\"~:r1\",8,\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cff458\",\"~:parent-id\",\"~u77c71dba-32ee-804c-8007-736561cf8584\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u77c71dba-32ee-804c-8007-736561cf8584\",\"~:strokes\",[],\"~:x\",695.9999775886536,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",420.00000357564704,\"^E\",66,\"~:height\",32,\"~:x1\",695.9999775886536,\"~:y1\",420.00000357564704,\"~:x2\",761.9999775886536,\"~:y2\",452.00000357564704]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#7efff5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^17\",32,\"~:flip-y\",null,\"~:shapes\",[\"~u77c71dba-32ee-804c-8007-736561cff459\"]]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cf857f": "[\"~#shape\",[\"^ \",\"~:y\",395.99997913999186,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",12,\"~:p3\",0,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:wrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Board Parent 1\",\"~:layout-align-items\",\"~:start\",\"~:width\",272,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",593.0000386238098,\"~:y\",395.99997913999186]],[\"^J\",[\"^ \",\"~:x\",865.0000386238098,\"~:y\",395.99997913999186]],[\"^J\",[\"^ \",\"~:x\",865.0000386238098,\"~:y\",475.9999669761459]],[\"^J\",[\"^ \",\"~:x\",593.0000386238098,\"~:y\",475.9999669761459]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cf857f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",593.0000386238098,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",593.0000386238098,\"~:y\",395.99997913999186,\"^D\",272,\"~:height\",79.99998783615405,\"~:x1\",593.0000386238098,\"~:y1\",395.99997913999186,\"~:x2\",865.0000386238098,\"~:y2\",475.9999669761459]],\"~:fills\",[],\"~:flip-x\",null,\"^15\",79.99998783615405,\"~:flip-y\",null,\"~:shapes\",[\"~u77c71dba-32ee-804c-8007-736561cf8584\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d50980078e": "[\"~#shape\",[\"^ \",\"~:y\",720.0000478045426,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",12,\"~:p3\",0,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:wrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Board Parent 4\",\"~:layout-align-items\",\"~:start\",\"~:width\",272,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",592.9998555183411,\"~:y\",720.0000478045426]],[\"^J\",[\"^ \",\"~:x\",864.9998555183411,\"~:y\",720.0000478045426]],[\"^J\",[\"^ \",\"~:x\",864.9998555183411,\"~:y\",800.0000356406968]],[\"^J\",[\"^ \",\"~:x\",592.9998555183411,\"~:y\",800.0000356406968]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d50980078e\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column-reverse\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",592.9998555183411,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",592.9998555183411,\"~:y\",720.0000478045426,\"^D\",272,\"~:height\",79.9999878361541,\"~:x1\",592.9998555183411,\"~:y1\",720.0000478045426,\"~:x2\",864.9998555183411,\"~:y2\",800.0000356406968]],\"~:fills\",[],\"~:flip-x\",null,\"^15\",79.9999878361541,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d50980078f\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d50980078f": "[\"~#shape\",[\"^ \",\"~:y\",719.9999806874634,\"~: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,\"~:hide-in-viewer\",true,\"~:name\",\"Board Child\",\"~:width\",80,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",719.9999806874634]],[\"^;\",[\"^ \",\"~:x\",684.9999775886536,\"~:y\",719.9999806874634]],[\"^;\",[\"^ \",\"~:x\",684.9999775886536,\"~:y\",799.9999806874634]],[\"^;\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",799.9999806874634]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d50980078f\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078e\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078e\",\"~:strokes\",[],\"~:x\",604.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",719.9999806874634,\"^7\",80,\"~:height\",80,\"~:x1\",604.9999775886536,\"~:y1\",719.9999806874634,\"~:x2\",684.9999775886536,\"~:y2\",799.9999806874634]],\"~:fills\",[],\"~:flip-x\",null,\"^K\",80,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d509800790\",\"~u94eaebe4-addd-80d1-8007-79d509800791\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508a9dc2f": "[\"~#shape\",[\"^ \",\"~:y\",612.000024916359,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",12,\"~:p3\",0,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:wrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Board Parent 3\",\"~:layout-align-items\",\"~:start\",\"~:width\",272,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",592.9999165534973,\"~:y\",612.000024916359]],[\"^J\",[\"^ \",\"~:x\",864.9999165534973,\"~:y\",612.000024916359]],[\"^J\",[\"^ \",\"~:x\",864.9999165534973,\"~:y\",692.0000127525132]],[\"^J\",[\"^ \",\"~:x\",592.9999165534973,\"~:y\",692.0000127525132]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc2f\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",592.9999165534973,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",592.9999165534973,\"~:y\",612.000024916359,\"^D\",272,\"~:height\",79.9999878361541,\"~:x1\",592.9999165534973,\"~:y1\",612.000024916359,\"~:x2\",864.9999165534973,\"~:y2\",692.0000127525132]],\"~:fills\",[],\"~:flip-x\",null,\"^15\",79.9999878361541,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d509800790": "[\"~#shape\",[\"^ \",\"~:y\",720.0000417226197,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",80,\"~:transforming\",false,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",604.9998555183411,\"~:y\",720.0000417226197]],[\"^>\",[\"^ \",\"~:x\",684.9998555183411,\"~:y\",720.0000417226197]],[\"^>\",[\"^ \",\"~:x\",684.9998555183411,\"~:y\",800.0000417226197]],[\"^>\",[\"^ \",\"~:x\",604.9998555183411,\"~:y\",800.0000417226197]]],\"~:r2\",8,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d509800790\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078f\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078f\",\"~:strokes\",[],\"~:x\",604.9998555183411,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",604.9998555183411,\"~:y\",720.0000417226197,\"^9\",80,\"~:height\",80,\"~:x1\",604.9998555183411,\"~:y1\",720.0000417226197,\"~:x2\",684.9998555183411,\"~:y2\",800.0000417226197]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e8e9ea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^M\",80,\"~:flip-y\",null]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d508a9dc30": "[\"~#shape\",[\"^ \",\"~:y\",612.0000188344361,\"~: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,\"~:hide-in-viewer\",true,\"~:name\",\"Board Child\",\"~:width\",80,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",612.0000188344361]],[\"^;\",[\"^ \",\"~:x\",684.9999775886536,\"~:y\",612.0000188344361]],[\"^;\",[\"^ \",\"~:x\",684.9999775886536,\"~:y\",692.0000188344361]],[\"^;\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",692.0000188344361]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc30\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc2f\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d508a9dc2f\",\"~:strokes\",[],\"~:x\",604.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",604.9999775886536,\"~:y\",612.0000188344361,\"^7\",80,\"~:height\",80,\"~:x1\",604.9999775886536,\"~:y1\",612.0000188344361,\"~:x2\",684.9999775886536,\"~:y2\",692.0000188344361]],\"~:fills\",[],\"~:flip-x\",null,\"^K\",80,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d508aa2885\",\"~u94eaebe4-addd-80d1-8007-79d508aa2886\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d509800791": "[\"~#shape\",[\"^ \",\"~:y\",744.0000417226197,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:rx\",8,\"~:layout-padding\",[\"^ \",\"~:p1\",8,\"~:p2\",12,\"~:p3\",8,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Dark / Button / Primary / Text / Default\",\"~:layout-align-items\",\"~:center\",\"~:width\",66,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",611.9998555183411,\"~:y\",744.0000417226197]],[\"^K\",[\"^ \",\"~:x\",677.9998555183411,\"~:y\",744.0000417226197]],[\"^K\",[\"^ \",\"~:x\",677.9998555183411,\"~:y\",776.0000417226197]],[\"^K\",[\"^ \",\"~:x\",611.9998555183411,\"~:y\",776.0000417226197]]],\"~:r2\",8,\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^;\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:layout-justify-content\",\"^D\",\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d509800791\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078f\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d50980078f\",\"~:strokes\",[],\"~:x\",611.9998555183411,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",611.9998555183411,\"~:y\",744.0000417226197,\"^E\",66,\"~:height\",32,\"~:x1\",611.9998555183411,\"~:y1\",744.0000417226197,\"~:x2\",677.9998555183411,\"~:y2\",776.0000417226197]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#7efff5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^17\",32,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d509800792\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d509800792": "[\"~#shape\",[\"^ \",\"~:y\",752.0000417226197,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",2,\"~:p3\",0,\"~:p4\",2],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"_Utilities / Text / White\",\"~:layout-align-items\",\"~:start\",\"~:width\",42,\"~:layout-padding-type\",\"~:simple\",\"~:transforming\",false,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",623.9998555183411,\"~:y\",752.0000417226197]],[\"^K\",[\"^ \",\"~:x\",665.9998555183411,\"~:y\",752.0000417226197]],[\"^K\",[\"^ \",\"~:x\",665.9998555183411,\"~:y\",768.0000417226197]],[\"^K\",[\"^ \",\"~:x\",623.9998555183411,\"~:y\",768.0000417226197]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",6],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d509800792\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d509800791\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d509800791\",\"~:strokes\",[],\"~:x\",623.9998555183411,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",623.9998555183411,\"~:y\",752.0000417226197,\"^D\",42,\"~:height\",16,\"~:x1\",623.9998555183411,\"~:y1\",752.0000417226197,\"~:x2\",665.9998555183411,\"~:y2\",768.0000417226197]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",16,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d509800793\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d509800793": "[\"~#shape\",[\"^ \",\"~:y\",753.0000417226199,\"~: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\",\"~:index\",null,\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^8\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:path\",\"\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"\",\"^<\",\"normal\",\"~:text-transform\",\"uppercase\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-work-sans\",\"~:font-size\",\"12\",\"~:font-weight\",\"500\",\"~:modified-at\",\"2024-06-04T14:15:09.786Z\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"underline\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-color-ref-file\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"~:fill-opacity\",1,\"~:fill-color-ref-id\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:font-family\",\"Work Sans\",\"~:text\",\"Label\"]],\"^=\",\"uppercase\",\"^>\",\"center\",\"^?\",\"gfont-work-sans\",\"^@\",\"12\",\"^A\",\"500\",\"^8\",\"paragraph\",\"^B\",\"2024-06-04T14:15:09.786Z\",\"^C\",\"500\",\"^D\",\"underline\",\"^E\",\"0\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"^K\",\"Work Sans\"]]]],\"~:vertical-align\",\"center\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^I\",1]]],\"~:hide-in-viewer\",true,\"~:name\",\"Input\",\"~:saved-component-root\",null,\"~:width\",38,\"^8\",\"^L\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",625.9998555183411,\"~:y\",753.0000417226199]],[\"^S\",[\"^ \",\"~:x\",663.9998555183411,\"~:y\",753.0000417226199]],[\"^S\",[\"^ \",\"~:x\",663.9998555183411,\"~:y\",768.0000417226195]],[\"^S\",[\"^ \",\"~:x\",625.9998555183411,\"~:y\",768.0000417226199]]],\"~:layout-item-h-sizing\",\"~:fix\",\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d509800793\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d509800792\",\"~:position-data\",[[\"^ \",\"~:y\",767.340087890625,\"^:\",\"1.2\",\"^<\",\"normal\",\"^=\",\"uppercase\",\"^>\",\"left\",\"^?\",\"sourcesanspro\",\"^@\",\"12\",\"^A\",\"500\",\"~:text-direction\",\"ltr\",\"^Q\",37.93994140625,\"^C\",\"regular\",\"^D\",\"underline\",\"^E\",\"0\",\"~:x\",626.0299072265625,\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:direction\",\"ltr\",\"^K\",\"Work Sans\",\"~:height\",14.08001708984375,\"^L\",\"Label\"]],\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d509800792\",\"~:strokes\",[],\"~:x\",625.9998555183411,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",625.9998555183411,\"~:y\",753.0000417226199,\"^Q\",38,\"^11\",15,\"~:x1\",625.9998555183411,\"~:y1\",753.0000417226199,\"~:x2\",663.9998555183411,\"~:y2\",768.0000417226199]],\"^F\",[],\"~:flip-x\",null,\"^11\",15,\"~:flip-y\",null]]",
|
||||
"~u77c71dba-32ee-804c-8007-736561cf8584": "[\"~#shape\",[\"^ \",\"~:y\",396.00000357564704,\"~: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,\"~:hide-in-viewer\",true,\"~:name\",\"Board Child\",\"~:width\",80,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",396.00000357564704]],[\"^;\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",396.00000357564704]],[\"^;\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",476.00000357564704]],[\"^;\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",476.00000357564704]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u77c71dba-32ee-804c-8007-736561cf8584\",\"~:parent-id\",\"~u77c71dba-32ee-804c-8007-736561cf857f\",\"~:frame-id\",\"~u77c71dba-32ee-804c-8007-736561cf857f\",\"~:strokes\",[],\"~:x\",688.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",396.00000357564704,\"^7\",80,\"~:height\",80,\"~:x1\",688.9999775886536,\"~:y1\",396.00000357564704,\"~:x2\",768.9999775886536,\"~:y2\",476.00000357564704]],\"~:fills\",[],\"~:flip-x\",null,\"^K\",80,\"~:flip-y\",null,\"~:shapes\",[\"~u77c71dba-32ee-804c-8007-736561cff457\",\"~u77c71dba-32ee-804c-8007-736561cff458\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d6859": "[\"~#shape\",[\"^ \",\"~:y\",504.00000202817546,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",12,\"~:p3\",0,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:wrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Board Parent 2\",\"~:layout-align-items\",\"~:start\",\"~:width\",272,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",592.9999775886536,\"~:y\",504.00000202817546]],[\"^J\",[\"^ \",\"~:x\",864.9999775886536,\"~:y\",504.00000202817546]],[\"^J\",[\"^ \",\"~:x\",864.9999775886536,\"~:y\",583.9999898643296]],[\"^J\",[\"^ \",\"~:x\",592.9999775886536,\"~:y\",583.9999898643296]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-item-v-sizing\",\"~:fix\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d6859\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:row-reverse\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",592.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",592.9999775886536,\"~:y\",504.00000202817546,\"^D\",272,\"~:height\",79.9999878361541,\"~:x1\",592.9999775886536,\"~:y1\",504.00000202817546,\"~:x2\",864.9999775886536,\"~:y2\",583.9999898643296]],\"~:fills\",[],\"~:flip-x\",null,\"^15\",79.9999878361541,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d5055d685a\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d685a": "[\"~#shape\",[\"^ \",\"~:y\",503.9999959462525,\"~: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,\"~:hide-in-viewer\",true,\"~:name\",\"Board Child\",\"~:width\",80,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",503.9999959462525]],[\"^;\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",503.9999959462525]],[\"^;\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",583.9999959462525]],[\"^;\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",583.9999959462525]]],\"~:show-content\",true,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:constraints-v\",\"~:top\",\"~:constraints-h\",\"~:left\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685a\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d6859\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d6859\",\"~:strokes\",[],\"~:x\",688.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",503.9999959462525,\"^7\",80,\"~:height\",80,\"~:x1\",688.9999775886536,\"~:y1\",503.9999959462525,\"~:x2\",768.9999775886536,\"~:y2\",583.9999959462525]],\"~:fills\",[],\"~:flip-x\",null,\"^K\",80,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d5055d685b\",\"~u94eaebe4-addd-80d1-8007-79d5055d685c\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d685b": "[\"~#shape\",[\"^ \",\"~:y\",503.9999959462525,\"~:rx\",8,\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:grow-type\",\"~:fixed\",\"~:hide-in-viewer\",false,\"~:name\",\"Rectangle\",\"~:width\",80,\"~:transforming\",false,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",503.9999959462525]],[\"^>\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",503.9999959462525]],[\"^>\",[\"^ \",\"~:x\",768.9999775886536,\"~:y\",583.9999959462525]],[\"^>\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",583.9999959462525]]],\"~:r2\",8,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^3\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685b\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685a\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685a\",\"~:strokes\",[],\"~:x\",688.9999775886536,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",688.9999775886536,\"~:y\",503.9999959462525,\"^9\",80,\"~:height\",80,\"~:x1\",688.9999775886536,\"~:y1\",503.9999959462525,\"~:x2\",768.9999775886536,\"~:y2\",583.9999959462525]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#e8e9ea\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^M\",80,\"~:flip-y\",null]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d685c": "[\"~#shape\",[\"^ \",\"~:y\",527.9999959462525,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:rx\",8,\"~:layout-padding\",[\"^ \",\"~:p1\",8,\"~:p2\",12,\"~:p3\",8,\"~:p4\",12],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"Dark / Button / Primary / Text / Default\",\"~:layout-align-items\",\"~:center\",\"~:width\",66,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",527.9999959462525]],[\"^K\",[\"^ \",\"~:x\",761.9999775886536,\"~:y\",527.9999959462525]],[\"^K\",[\"^ \",\"~:x\",761.9999775886536,\"~:y\",559.9999959462525]],[\"^K\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",559.9999959462525]]],\"~:r2\",8,\"~:show-content\",true,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",4,\"~:column-gap\",4],\"~:transform-inverse\",[\"^;\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:r3\",8,\"~:layout-justify-content\",\"^D\",\"~:r1\",8,\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685c\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685a\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685a\",\"~:strokes\",[],\"~:x\",695.9999775886536,\"~:proportion\",1,\"~:r4\",8,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",695.9999775886536,\"~:y\",527.9999959462525,\"^E\",66,\"~:height\",32,\"~:x1\",695.9999775886536,\"~:y1\",527.9999959462525,\"~:x2\",761.9999775886536,\"~:y2\",559.9999959462525]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#7efff5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"~:ry\",8,\"^17\",32,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d5055d685d\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d685d": "[\"~#shape\",[\"^ \",\"~:y\",535.9999959462525,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",2,\"~:p3\",0,\"~:p4\",2],\"~:transform\",[\"~#matrix\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:rotation\",0,\"~:layout-wrap-type\",\"~:nowrap\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",true,\"~:name\",\"_Utilities / Text / White\",\"~:layout-align-items\",\"~:start\",\"~:width\",42,\"~:layout-padding-type\",\"~:simple\",\"~:transforming\",false,\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",535.9999959462525]],[\"^K\",[\"^ \",\"~:x\",749.9999775886536,\"~:y\",535.9999959462525]],[\"^K\",[\"^ \",\"~:x\",749.9999775886536,\"~:y\",551.9999959462525]],[\"^K\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",551.9999959462525]]],\"~:show-content\",true,\"~:layout-item-h-sizing\",\"~:auto\",\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",6],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:layout-justify-content\",\"~:center\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685d\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685c\",\"~:layout-flex-dir\",\"~:column\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685c\",\"~:strokes\",[],\"~:x\",707.9999775886536,\"~:proportion\",1,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",707.9999775886536,\"~:y\",535.9999959462525,\"^D\",42,\"~:height\",16,\"~:x1\",707.9999775886536,\"~:y1\",535.9999959462525,\"~:x2\",749.9999775886536,\"~:y2\",551.9999959462525]],\"~:fills\",[],\"~:flip-x\",null,\"^16\",16,\"~:flip-y\",null,\"~:shapes\",[\"~u94eaebe4-addd-80d1-8007-79d5055d685e\"]]]",
|
||||
"~u94eaebe4-addd-80d1-8007-79d5055d685e": "[\"~#shape\",[\"^ \",\"~:y\",536.9999959462527,\"~: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\",\"~:index\",null,\"~:content\",[\"^ \",\"~:type\",\"root\",\"~:children\",[[\"^ \",\"^8\",\"paragraph-set\",\"^9\",[[\"^ \",\"~:line-height\",\"1.2\",\"~:path\",\"\",\"~:font-style\",\"normal\",\"^9\",[[\"^ \",\"^:\",\"1.2\",\"^;\",\"\",\"^<\",\"normal\",\"~:text-transform\",\"uppercase\",\"~:text-align\",\"left\",\"~:font-id\",\"gfont-work-sans\",\"~:font-size\",\"12\",\"~:font-weight\",\"500\",\"~:modified-at\",\"2024-06-04T14:15:09.786Z\",\"~:font-variant-id\",\"500\",\"~:text-decoration\",\"underline\",\"~:letter-spacing\",\"0\",\"~:fills\",[[\"^ \",\"~:fill-color\",\"#000000\",\"~:fill-color-ref-file\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"~:fill-opacity\",1,\"~:fill-color-ref-id\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:font-family\",\"Work Sans\",\"~:text\",\"Label\"]],\"^=\",\"uppercase\",\"^>\",\"center\",\"^?\",\"gfont-work-sans\",\"^@\",\"12\",\"^A\",\"500\",\"^8\",\"paragraph\",\"^B\",\"2024-06-04T14:15:09.786Z\",\"^C\",\"500\",\"^D\",\"underline\",\"^E\",\"0\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"^K\",\"Work Sans\"]]]],\"~:vertical-align\",\"center\",\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^I\",1]]],\"~:hide-in-viewer\",true,\"~:name\",\"Input\",\"~:saved-component-root\",null,\"~:width\",38,\"^8\",\"^L\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",536.9999959462527]],[\"^S\",[\"^ \",\"~:x\",747.9999775886536,\"~:y\",536.9999959462527]],[\"^S\",[\"^ \",\"~:x\",747.9999775886536,\"~:y\",551.9999959462523]],[\"^S\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",551.9999959462527]]],\"~:layout-item-h-sizing\",\"~:fix\",\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:page-id\",\"~u02e9633d-4ce7-80da-8007-736558496fa8\",\"~:id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685e\",\"~:parent-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685d\",\"~:position-data\",[[\"^ \",\"~:y\",551.3400268554688,\"^:\",\"1.2\",\"^<\",\"normal\",\"^=\",\"uppercase\",\"^>\",\"left\",\"^?\",\"sourcesanspro\",\"^@\",\"12\",\"^A\",\"500\",\"~:text-direction\",\"ltr\",\"^Q\",37.93994140625,\"^C\",\"regular\",\"^D\",\"underline\",\"^E\",\"0\",\"~:x\",710.030029296875,\"^F\",[[\"^ \",\"^G\",\"#000000\",\"^H\",\"~ucaa70d02-51e1-81ae-8007-735e7de3d7bc\",\"^I\",1,\"^J\",\"~udfa92acf-7d18-8079-8003-baba8789d8af\"]],\"~:direction\",\"ltr\",\"^K\",\"Work Sans\",\"~:height\",14.08001708984375,\"^L\",\"Label\"]],\"~:frame-id\",\"~u94eaebe4-addd-80d1-8007-79d5055d685d\",\"~:strokes\",[],\"~:x\",709.9999775886536,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",709.9999775886536,\"~:y\",536.9999959462527,\"^Q\",38,\"^11\",15,\"~:x1\",709.9999775886536,\"~:y1\",536.9999959462527,\"~:x2\",747.9999775886536,\"~:y2\",551.9999959462527]],\"^F\",[],\"~:flip-x\",null,\"^11\",15,\"~:flip-y\",null]]"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"~:id": "~u31fe2e21-73e7-80f3-8007-73894fb58240",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -58,10 +58,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
||||
|
||||
async waitForTextSpan(nth = 0) {
|
||||
if (!nth) {
|
||||
return this.page.waitForSelector('[data-itype="span"]');
|
||||
return this.page.waitForSelector('[data-itype="inline"]');
|
||||
}
|
||||
return this.page.waitForSelector(
|
||||
`[data-itype="span"]:nth-child(${nth})`,
|
||||
`[data-itype="inline"]:nth-child(${nth})`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -210,22 +210,6 @@ test("Renders a file with shadows applied to any kind of shape", async ({
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with flex layouts and different directions", async ({
|
||||
page,
|
||||
}) => {
|
||||
const workspace = new WasmWorkspacePage(page);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("render-wasm/get-file-flex-layouts.json");
|
||||
|
||||
await workspace.goToWorkspace({
|
||||
id: "31fe2e21-73e7-80f3-8007-73894fb58240",
|
||||
pageId: "02e9633d-4ce7-80da-8007-736558496fa8",
|
||||
});
|
||||
await workspace.waitForFirstRenderWithoutUI();
|
||||
|
||||
await expect(workspace.canvas).toHaveScreenshot();
|
||||
});
|
||||
|
||||
test("Renders a file with a closed path shape with multiple segments using strokes and shadow", async ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 18 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 23 KiB |
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, draw_star,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, allocBytes,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import {
|
||||
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
|
||||
} from './js/lib.js';
|
||||
@@ -102,4 +102,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -61,11 +61,6 @@
|
||||
;; Def micro-benchmark iterations
|
||||
(def micro-benchmark-iterations 1e6)
|
||||
|
||||
;; Performance logs
|
||||
(defonce ^:private longtask-observer* (atom nil))
|
||||
(defonce ^:private stall-timer* (atom nil))
|
||||
(defonce ^:private current-op* (atom nil))
|
||||
|
||||
;; --- CONTEXT
|
||||
|
||||
(defn- collect-context
|
||||
@@ -469,75 +464,3 @@
|
||||
(defn event
|
||||
[props]
|
||||
(ptk/data-event ::event props))
|
||||
|
||||
;; --- DEVTOOLS PERF LOGGING
|
||||
|
||||
(defn install-long-task-observer! []
|
||||
(when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*))
|
||||
(let [observer (js/PerformanceObserver.
|
||||
(fn [list _]
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(doseq [entry (.getEntries list)]
|
||||
(let [dur (.-duration entry)
|
||||
start (.-startTime entry)
|
||||
attrib (.-attribution entry)
|
||||
attrib-count (when attrib (.-length attrib))
|
||||
first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0))
|
||||
attrib-name (when first-attrib (.-name first-attrib))
|
||||
attrib-ctype (when first-attrib (.-containerType first-attrib))
|
||||
attrib-cid (when first-attrib (.-containerId first-attrib))
|
||||
attrib-csrc (when first-attrib (.-containerSrc first-attrib))]
|
||||
|
||||
(.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms"
|
||||
(when first-attrib
|
||||
(str " attrib:name=" attrib-name
|
||||
" ctype=" attrib-ctype
|
||||
" cid=" attrib-cid
|
||||
" csrc=" attrib-csrc)))))))))]
|
||||
(.observe observer #js{:entryTypes #js["longtask"]})
|
||||
(reset! longtask-observer* observer))))
|
||||
|
||||
(defn start-event-loop-stall-logger!
|
||||
"Log event loop stalls by measuring setInterval drift.
|
||||
interval-ms: base interval
|
||||
threshold-ms: drift over which we report"
|
||||
[interval-ms threshold-ms]
|
||||
(when (nil? @stall-timer*)
|
||||
(let [last (atom (.now js/performance))
|
||||
id (js/setInterval
|
||||
(fn []
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(let [now (.now js/performance)
|
||||
expected (+ @last interval-ms)
|
||||
drift (- now expected)
|
||||
current-op @current-op*
|
||||
measures (.getEntriesByType js/performance "measure")
|
||||
mlen (.-length measures)
|
||||
last-measure (when (> mlen 0) (aget measures (dec mlen)))
|
||||
meas-name (when last-measure (.-name last-measure))
|
||||
meas-detail (when last-measure (.-detail last-measure))
|
||||
meas-count (when meas-detail (unchecked-get meas-detail "count"))]
|
||||
(reset! last now)
|
||||
(when (> drift threshold-ms)
|
||||
(.warn js/console
|
||||
(str "[perf] event loop stall: " (Math/round drift) "ms"
|
||||
(when current-op (str " op=" current-op))
|
||||
(when meas-name (str " last=" meas-name))
|
||||
(when meas-count (str " count=" meas-count))))))))
|
||||
interval-ms)]
|
||||
(reset! stall-timer* id))))
|
||||
|
||||
(defn init!
|
||||
"Install perf observers in dev builds. Safe to call multiple times.
|
||||
Perf logs are disabled by default. Enable them with the :perf-logs flag in config."
|
||||
[]
|
||||
#_(when ^boolean js/goog.DEBUG
|
||||
(install-long-task-observer!)
|
||||
(start-event-loop-stall-logger! 50 100)
|
||||
;; Expose simple API on window for manual control in devtools
|
||||
(let [api #js {:reset (fn []
|
||||
(try
|
||||
(.clearMarks js/performance)
|
||||
(.clearMeasures js/performance)
|
||||
(catch :default _ nil)))}]
|
||||
(aset js/window "PenpotPerf" api))))
|
||||
|
||||
@@ -347,12 +347,6 @@
|
||||
(with-meta {:team-id team-id
|
||||
:file-id file-id}))))))
|
||||
|
||||
;; Install dev perf observers once the workspace is ready
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::workspace-initialized))
|
||||
(rx/take 1)
|
||||
(rx/map (fn [_] (ev/init!))))
|
||||
|
||||
(->> stream
|
||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||
(rx/take 1)
|
||||
|
||||
@@ -18,13 +18,13 @@
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [expand-fn (fn [expanded]
|
||||
(let [parents-seqs (map (fn [x] (cfh/get-parent-ids objects x)) ids)
|
||||
flat-parents (apply concat parents-seqs)
|
||||
non-root-parents (remove #(= % uuid/zero) flat-parents)
|
||||
distinct-parents (into #{} non-root-parents)]
|
||||
(merge expanded
|
||||
(into {}
|
||||
(map (fn [id] {id true}) distinct-parents)))))]
|
||||
(merge expanded
|
||||
(->> ids
|
||||
(map #(cfh/get-parent-ids objects %))
|
||||
flatten
|
||||
(remove #(= % uuid/zero))
|
||||
(map (fn [id] {id true}))
|
||||
(into {}))))]
|
||||
(update-in state [:workspace-local :expanded] expand-fn)))))
|
||||
|
||||
|
||||
|
||||
@@ -214,8 +214,8 @@
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [change-fn
|
||||
(fn [node attrs]
|
||||
(update node :fills types.fills/prepend attrs))
|
||||
(fn [shape attrs]
|
||||
(update shape :fills types.fills/prepend attrs))
|
||||
undo-id
|
||||
(js/Symbol)]
|
||||
(rx/concat
|
||||
|
||||
@@ -616,7 +616,7 @@
|
||||
[modif-tree & {:keys [ignore-constraints ignore-snap-pixel snap-ignore-axis undo-transation?]
|
||||
:or {ignore-constraints false ignore-snap-pixel false snap-ignore-axis nil undo-transation? true}
|
||||
:as params}]
|
||||
(ptk/reify ::apply-wasm-modifiers
|
||||
(ptk/reify ::apply-wasm-modifiesr
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(wasm.api/clean-modifiers)
|
||||
|
||||
@@ -105,15 +105,9 @@
|
||||
(if (dsh/lookup-page state file-id page-id)
|
||||
(rx/concat
|
||||
(rx/of (initialize-page* file-id page-id)
|
||||
(fdf/fix-deleted-fonts-for-page file-id page-id))
|
||||
|
||||
;; Disable thumbnail generation in wasm renderer
|
||||
(if (features/active-feature? state "render-wasm/v1")
|
||||
(rx/empty)
|
||||
(rx/of (dwth/watch-state-changes file-id page-id)))
|
||||
|
||||
(rx/of (dwl/watch-component-changes))
|
||||
|
||||
(fdf/fix-deleted-fonts-for-page file-id page-id)
|
||||
(dwth/watch-state-changes file-id page-id)
|
||||
(dwl/watch-component-changes))
|
||||
(let [profile (:profile state)
|
||||
props (get profile :props)]
|
||||
(when (not (:workspace-visited props))
|
||||
|
||||
@@ -70,20 +70,22 @@
|
||||
(= (-> content last :command) :move-to))
|
||||
(into [] (take (dec (count content)) content))
|
||||
content)]
|
||||
(-> state
|
||||
(st/set-content content))))
|
||||
(st/set-content state content)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
id (dm/get-in state [:workspace-local :edition])
|
||||
old-content (dm/get-in state [:workspace-local :edit-path id :old-content])
|
||||
shape (st/get-path state)]
|
||||
local (get state :workspace-local)
|
||||
id (get local :edition)
|
||||
objects (dsh/lookup-page-objects state page-id)]
|
||||
|
||||
(if (and (some? old-content) (some? (:id shape)))
|
||||
(let [changes (generate-path-changes it objects page-id shape old-content (:content shape))]
|
||||
(rx/of (dch/commit-changes changes)))
|
||||
(rx/empty)))))))
|
||||
;; NOTE: we proceed only if the shape is present on the
|
||||
;; objects, if shape is a ephimeral drawing shape, we should
|
||||
;; do nothing
|
||||
(when-let [shape (get objects id)]
|
||||
(when-let [old-content (dm/get-in local [:edit-path id :old-content])]
|
||||
(let [new-content (get shape :content)
|
||||
changes (generate-path-changes it objects page-id shape old-content new-content)]
|
||||
(rx/of (dch/commit-changes changes))))))))))
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(: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.types.path :as path]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
@@ -289,34 +288,34 @@
|
||||
|
||||
(declare stop-path-edit)
|
||||
|
||||
|
||||
(defn start-path-edit
|
||||
[id]
|
||||
(ptk/reify ::start-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
edit-path (dm/get-in state [:workspace-local :edit-path id])
|
||||
content (st/get-path state :content)
|
||||
state (cond-> state
|
||||
(cfh/path-shape? objects id)
|
||||
(st/set-content (path/close-subpaths content)))]
|
||||
shape (get objects id)]
|
||||
|
||||
(cond-> state
|
||||
(or (not edit-path)
|
||||
(= :draw (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id] {:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})
|
||||
(and (some? edit-path)
|
||||
(= :move (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
|
||||
(-> state
|
||||
(st/set-content (path/close-subpaths (:content shape)))
|
||||
(update-in [:workspace-local :edit-path id]
|
||||
(fn [state]
|
||||
(let [state (if state
|
||||
(if (= :move (:edit-mode state))
|
||||
(assoc state :edit-mode :draw)
|
||||
state)
|
||||
{:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})]
|
||||
(assoc state :old-content (:content shape))))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))))]
|
||||
(let [stopper (rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))
|
||||
stream)]
|
||||
(rx/concat
|
||||
(rx/of (undo/start-path-undo))
|
||||
(->> stream
|
||||
@@ -325,7 +324,8 @@
|
||||
(rx/map #(stop-path-edit id))
|
||||
(rx/take-until stopper)))))))
|
||||
|
||||
(defn stop-path-edit [id]
|
||||
(defn stop-path-edit
|
||||
[id]
|
||||
(ptk/reify ::stop-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -335,13 +335,12 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (ptk/data-event :layout/update {:ids [id]})))))
|
||||
|
||||
(defn split-segments
|
||||
[{:keys [from-p to-p t]}]
|
||||
(defn- split-segments
|
||||
[id {:keys [from-p to-p t]}]
|
||||
(ptk/reify ::split-segments
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (st/get-path-id state)
|
||||
content (st/get-path state :content)]
|
||||
(let [content (st/get-path state :content)]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :edit-path id :old-content] content)
|
||||
(st/set-content (-> content
|
||||
@@ -353,10 +352,10 @@
|
||||
(rx/of (changes/save-path-content {:preserve-move-to true})))))
|
||||
|
||||
(defn create-node-at-position
|
||||
[event]
|
||||
[params]
|
||||
(ptk/reify ::create-node-at-position
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [id (st/get-path-id state)]
|
||||
(rx/of (dwsh/update-shapes [id] path/convert-to-path)
|
||||
(split-segments event))))))
|
||||
(split-segments id params))))))
|
||||
|
||||
@@ -264,13 +264,10 @@
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
;; Schedule expanding parents asynchronously to avoid blocking
|
||||
;; the event loop
|
||||
expand-s (->> (rx/of (dwc/expand-all-parents ids objects))
|
||||
(rx/observe-on :async))
|
||||
interrupt-s (rx/of ::dwsp/interrupt)]
|
||||
(rx/merge expand-s interrupt-s)))))
|
||||
(let [objects (dsh/lookup-page-objects state)]
|
||||
(rx/of
|
||||
(dwc/expand-all-parents ids objects)
|
||||
::dwsp/interrupt)))))
|
||||
|
||||
(defn select-all
|
||||
[]
|
||||
|
||||
@@ -794,7 +794,6 @@
|
||||
|
||||
(defn update-attrs
|
||||
[id attrs]
|
||||
(prn "update-attrs" id attrs)
|
||||
(ptk/reify ::update-attrs
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
@@ -821,7 +820,7 @@
|
||||
(when (features/active-feature? state "text-editor/v2")
|
||||
(rx/of (v2-update-text-editor-styles id attrs)))
|
||||
|
||||
#_(when (features/active-feature? state "render-wasm/v1")
|
||||
(when (features/active-feature? state "render-wasm/v1")
|
||||
;; This delay is to give time for the font to be correctly rendered
|
||||
;; in wasm.
|
||||
(cond->> (rx/of (resize-wasm-text id))
|
||||
|
||||
@@ -191,63 +191,59 @@
|
||||
[page-id [event [old-data new-data]]]
|
||||
|
||||
(let [changes (:changes event)
|
||||
;; cache for the get-frame-ids function
|
||||
frame-id-cache (atom {})]
|
||||
lookup-data-objects
|
||||
(fn [data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
(letfn [(lookup-data-objects [data page-id]
|
||||
(dm/get-in data [:pages-index page-id :objects]))
|
||||
|
||||
(extract-ids [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
:add-obj [[page-id (:id change)]]
|
||||
:mod-obj [[page-id (:id change)]]
|
||||
:del-obj [[page-id (:id change)]]
|
||||
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||
[]))
|
||||
extract-ids
|
||||
(fn [{:keys [page-id type] :as change}]
|
||||
(case type
|
||||
:add-obj [[page-id (:id change)]]
|
||||
:mod-obj [[page-id (:id change)]]
|
||||
:del-obj [[page-id (:id change)]]
|
||||
:mov-objects (->> (:shapes change) (map #(vector page-id %)))
|
||||
[]))
|
||||
|
||||
(get-frame-ids [id]
|
||||
(let [old-objects (lookup-data-objects old-data page-id)
|
||||
new-objects (lookup-data-objects new-data page-id)
|
||||
get-frame-ids
|
||||
(fn get-frame-ids [id]
|
||||
(let [old-objects (lookup-data-objects old-data page-id)
|
||||
new-objects (lookup-data-objects new-data page-id)
|
||||
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
new-shape (get new-objects id)
|
||||
old-shape (get old-objects id)
|
||||
|
||||
old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape))
|
||||
new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape))
|
||||
old-frame-id (if (cfh/frame-shape? old-shape) id (:frame-id old-shape))
|
||||
new-frame-id (if (cfh/frame-shape? new-shape) id (:frame-id new-shape))
|
||||
|
||||
root-frame-old? (cfh/root-frame? old-objects old-frame-id)
|
||||
root-frame-new? (cfh/root-frame? new-objects new-frame-id)
|
||||
instance-root? (ctc/instance-root? new-shape)]
|
||||
root-frame-old? (cfh/root-frame? old-objects old-frame-id)
|
||||
root-frame-new? (cfh/root-frame? new-objects new-frame-id)
|
||||
instance-root? (ctc/instance-root? new-shape)]
|
||||
|
||||
(cond-> #{}
|
||||
root-frame-old?
|
||||
(conj ["frame" old-frame-id])
|
||||
(cond-> #{}
|
||||
root-frame-old?
|
||||
(conj ["frame" old-frame-id])
|
||||
|
||||
root-frame-new?
|
||||
(conj ["frame" new-frame-id])
|
||||
root-frame-new?
|
||||
(conj ["frame" new-frame-id])
|
||||
|
||||
instance-root?
|
||||
(conj ["component" id])
|
||||
instance-root?
|
||||
(conj ["component" id])
|
||||
|
||||
(and (uuid? (:frame-id old-shape))
|
||||
(not= uuid/zero (:frame-id old-shape)))
|
||||
(into (get-frame-ids (:frame-id old-shape)))
|
||||
(and (uuid? (:frame-id old-shape))
|
||||
(not= uuid/zero (:frame-id old-shape)))
|
||||
(into (get-frame-ids (:frame-id old-shape)))
|
||||
|
||||
(and (uuid? (:frame-id new-shape))
|
||||
(not= uuid/zero (:frame-id new-shape)))
|
||||
(into (get-frame-ids (:frame-id new-shape))))))
|
||||
(and (uuid? (:frame-id new-shape))
|
||||
(not= uuid/zero (:frame-id new-shape)))
|
||||
(into (get-frame-ids (:frame-id new-shape))))))]
|
||||
|
||||
(get-frame-ids-cached [id]
|
||||
(or (get @frame-id-cache id)
|
||||
(let [result (get-frame-ids id)]
|
||||
(swap! frame-id-cache assoc id result)
|
||||
result)))]
|
||||
(into #{}
|
||||
(comp (mapcat extract-ids)
|
||||
(filter (fn [[page-id']] (= page-id page-id')))
|
||||
(map (fn [[_ id]] id))
|
||||
(mapcat get-frame-ids-cached))
|
||||
changes))))
|
||||
(into #{}
|
||||
(comp (mapcat extract-ids)
|
||||
(filter (fn [[page-id']] (= page-id page-id')))
|
||||
(map (fn [[_ id]] id))
|
||||
(mapcat get-frame-ids))
|
||||
changes)))
|
||||
|
||||
(defn watch-state-changes
|
||||
"Watch the state for changes inside frames. If a change is detected will force a rendering
|
||||
|
||||
@@ -27,10 +27,8 @@
|
||||
[app.main.data.workspace.colors :as wdc]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -302,20 +300,11 @@
|
||||
update-fn (fn [node _]
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node)))
|
||||
;; Check if any attribute affects text layout (requires resize)
|
||||
affects-layout? (some #(contains? txt-attrs %) [:font-size :font-family :font-weight :letter-spacing :line-height])]
|
||||
(ptk/reify ::generate-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(and affects-layout?
|
||||
(features/active-feature? state "render-wasm/v1"))
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(cty/remove-typography-from-node)))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
|
||||
(defn update-line-height
|
||||
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
||||
@@ -364,17 +353,11 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(ptk/reify ::generate-font-family-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
|
||||
(defn- create-font-family-text-attrs
|
||||
[value]
|
||||
@@ -442,16 +425,10 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(ptk/reify ::generate-font-weight-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
|
||||
(defn update-font-weight
|
||||
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
||||
|
||||
@@ -51,7 +51,7 @@
|
||||
|
||||
(or (> (:width srect) width)
|
||||
(> (:height srect) height))
|
||||
(let [srect (gal/adjust-to-viewport size srect {:padding 40 :min-zoom 0.01})
|
||||
(let [srect (gal/adjust-to-viewport size srect {:padding 40})
|
||||
zoom (/ (:width size) (:width srect))]
|
||||
|
||||
(-> local
|
||||
|
||||
@@ -97,7 +97,7 @@
|
||||
state
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160})
|
||||
zoom (/ (:width vport) (:width srect))]
|
||||
(-> local
|
||||
(assoc :zoom zoom)
|
||||
@@ -118,7 +118,7 @@
|
||||
(gsh/shapes->rect))]
|
||||
(update state :workspace-local
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
|
||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40})
|
||||
zoom (/ (:width vport) (:width srect))]
|
||||
(-> local
|
||||
(assoc :zoom zoom)
|
||||
@@ -142,7 +142,7 @@
|
||||
(fn [{:keys [vport] :as local}]
|
||||
(let [srect (gal/adjust-to-viewport
|
||||
vport srect
|
||||
{:padding 40 :min-zoom 0.01})
|
||||
{:padding 40})
|
||||
zoom (/ (:width vport)
|
||||
(:width srect))]
|
||||
(-> local
|
||||
|
||||
@@ -305,7 +305,7 @@
|
||||
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
|
||||
|
||||
(def workspace-page-objects
|
||||
(l/derived dsh/lookup-page-objects st/state identical?))
|
||||
(l/derived dsh/lookup-page-objects st/state))
|
||||
|
||||
(def workspace-read-only?
|
||||
(l/derived :read-only? workspace-global))
|
||||
|
||||
@@ -10,7 +10,6 @@ $z-index-200: 200;
|
||||
$z-index-300: 300;
|
||||
$z-index-400: 400;
|
||||
$z-index-500: 500;
|
||||
$z-index-600: 600;
|
||||
|
||||
:global(:root) {
|
||||
--z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ...
|
||||
@@ -19,5 +18,4 @@ $z-index-600: 600;
|
||||
--z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements
|
||||
--z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns
|
||||
--z-index-notifications: #{$z-index-500}; // Index for notification
|
||||
--z-index-loaders: #{$z-index-600}; // Index for loaders
|
||||
}
|
||||
|
||||
@@ -308,16 +308,6 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-click} (tr "labels.retry")]]]))
|
||||
|
||||
(mf/defc webgl-context-lost*
|
||||
[]
|
||||
(let [on-reload (mf/use-fn #(js/location.reload))]
|
||||
[:> error-container* {}
|
||||
[:div {:class (stl/css :main-message)} (tr "errors.webgl-context-lost.main-message")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "errors.webgl-context-lost.desc-message")]
|
||||
[:div {:class (stl/css :buttons-container)}
|
||||
[:> button* {:variant "primary" :on-click on-reload}
|
||||
(tr "labels.reload-page")]]]))
|
||||
|
||||
(defn- generate-report
|
||||
[data]
|
||||
(try
|
||||
@@ -447,7 +437,6 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
@@ -480,9 +469,6 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
|
||||
@@ -217,10 +217,6 @@
|
||||
|
||||
design-tokens? (features/use-feature "design-tokens/v1")
|
||||
|
||||
wasm-renderer-enabled? (features/use-feature "render-wasm/v1")
|
||||
|
||||
first-frame-rendered? (mf/use-state false)
|
||||
|
||||
background-color (:background-color wglobal)]
|
||||
|
||||
(mf/with-effect []
|
||||
@@ -245,17 +241,6 @@
|
||||
(when (and file-loaded? (not page-id))
|
||||
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
|
||||
|
||||
(mf/with-effect [file-id page-id]
|
||||
(reset! first-frame-rendered? false))
|
||||
|
||||
(mf/with-effect []
|
||||
(let [handle-wasm-render
|
||||
(fn [_]
|
||||
(reset! first-frame-rendered? true))
|
||||
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
|
||||
(fn []
|
||||
(events/unlistenByKey listener-key))))
|
||||
|
||||
[:> (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> (mf/provider ctx/current-file-id) {:value file-id}
|
||||
[:> (mf/provider ctx/current-page-id) {:value page-id}
|
||||
@@ -264,22 +249,15 @@
|
||||
[:> modal-container*]
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"
|
||||
:position "relative"}}
|
||||
:touch-action "none"}}
|
||||
[:> context-menu*]
|
||||
(when (and file-loaded? page-id)
|
||||
(if (and file-loaded? page-id)
|
||||
[:> workspace-inner*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}])
|
||||
(when (or (not (and file-loaded? page-id))
|
||||
;; in wasm renderer, extend the pixel loader until the first frame is rendered
|
||||
;; but do not apply it when switching pages
|
||||
(and wasm-renderer-enabled?
|
||||
(not file-loaded?)
|
||||
(not @first-frame-rendered?)))
|
||||
:layout layout}]
|
||||
[:> workspace-loader*])]]]]]]))
|
||||
|
||||
(mf/defc workspace-page*
|
||||
|
||||
@@ -20,13 +20,7 @@
|
||||
}
|
||||
|
||||
.workspace-loader {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--z-index-loaders);
|
||||
background-color: var(--color-background-primary);
|
||||
grid-area: viewport;
|
||||
}
|
||||
|
||||
.workspace-content {
|
||||
|
||||
@@ -90,8 +90,7 @@
|
||||
instance
|
||||
(dwt/create-editor editor-node canvas-node options)
|
||||
|
||||
;; Store original content to compare name later
|
||||
original-content content
|
||||
update-name? (nil? content)
|
||||
|
||||
on-key-up
|
||||
(fn [event]
|
||||
@@ -102,22 +101,10 @@
|
||||
on-blur
|
||||
(fn []
|
||||
(when-let [content (content/dom->cljs (dwt/get-editor-root instance))]
|
||||
(let [state @st/state
|
||||
objects (dsh/lookup-page-objects state)
|
||||
shape (get objects shape-id)
|
||||
current-name (:name shape)
|
||||
generated-name (gen-name instance)
|
||||
;; Update name if: (1) it's a new shape (nil original content), or
|
||||
;; (2) the current name matches the generated name from original content
|
||||
;; (meaning it was never manually renamed)
|
||||
update-name? (or (nil? original-content)
|
||||
(and (some? current-name)
|
||||
(some? original-content)
|
||||
(= current-name (txt/generate-shape-name (txt/content->text original-content)))))]
|
||||
(st/emit! (dwt/v2-update-text-shape-content shape-id content
|
||||
:update-name? update-name?
|
||||
:name generated-name
|
||||
:finalize? true))))
|
||||
(st/emit! (dwt/v2-update-text-shape-content shape-id content
|
||||
:update-name? update-name?
|
||||
:name (gen-name instance)
|
||||
:finalize? true)))
|
||||
|
||||
(let [container-node (mf/ref-val container-ref)]
|
||||
(dom/set-style! container-node "opacity" 0)))
|
||||
@@ -151,6 +138,7 @@
|
||||
(st/emit! (dw/set-clipboard-style style))))]
|
||||
|
||||
(.addEventListener ^js global/document "keyup" on-key-up)
|
||||
(.addEventListener ^js instance "blur" on-blur)
|
||||
(.addEventListener ^js instance "focus" on-focus)
|
||||
(.addEventListener ^js instance "needslayout" on-needs-layout)
|
||||
(.addEventListener ^js instance "stylechange" on-style-change)
|
||||
@@ -165,12 +153,8 @@
|
||||
|
||||
;; This function is called when the component is unmounted
|
||||
(fn []
|
||||
;; Explicitly call on-blur here instead of relying on browser blur events,
|
||||
;; because in Firefox blur is not reliably fired when leaving the text editor
|
||||
;; by clicking elsewhere. The component does unmount when the shape is
|
||||
;; deselected, so we can safely call the blur handler here to finalize the editor.
|
||||
(on-blur)
|
||||
(.removeEventListener ^js global/document "keyup" on-key-up)
|
||||
(.removeEventListener ^js instance "blur" on-blur)
|
||||
(.removeEventListener ^js instance "focus" on-focus)
|
||||
(.removeEventListener ^js instance "needslayout" on-needs-layout)
|
||||
(.removeEventListener ^js instance "stylechange" on-style-change)
|
||||
|
||||
@@ -33,24 +33,9 @@
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; Coalesce sidebar hover highlights to 1 frame to avoid long tasks
|
||||
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
|
||||
(defonce ^:private sidebar-hover-pending? (atom false))
|
||||
|
||||
(defn- schedule-sidebar-hover-flush []
|
||||
(when (compare-and-set! sidebar-hover-pending? false true)
|
||||
(ts/raf
|
||||
(fn []
|
||||
(let [{:keys [enter leave]} (swap! sidebar-hover-queue (constantly {:enter #{} :leave #{}}))]
|
||||
(reset! sidebar-hover-pending? false)
|
||||
(when (seq leave)
|
||||
(apply st/emit! (map dw/dehighlight-shape leave)))
|
||||
(when (seq enter)
|
||||
(apply st/emit! (map dw/highlight-shape enter))))))))
|
||||
|
||||
(mf/defc layer-item-inner
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [item depth parent-size name-ref children ref style
|
||||
[{:keys [item depth parent-size name-ref children ref
|
||||
;; Flags
|
||||
read-only? highlighted? selected? component-tree?
|
||||
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
||||
@@ -97,8 +82,7 @@
|
||||
:dnd-over dnd-over?
|
||||
:dnd-over-top dnd-over-top?
|
||||
:dnd-over-bot dnd-over-bot?
|
||||
:root-board parent-board?)
|
||||
:style style}
|
||||
:root-board parent-board?)}
|
||||
[:span {:class (stl/css-case
|
||||
:tab-indentation true
|
||||
:filtered filtered?)
|
||||
@@ -182,12 +166,10 @@
|
||||
|
||||
children]))
|
||||
|
||||
;; Memoized for performance
|
||||
(mf/defc layer-item
|
||||
{::mf/props :obj
|
||||
::mf/wrap [mf/memo]}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
|
||||
:or {render-children? true}}]
|
||||
::mf/memo true}
|
||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted]}]
|
||||
(let [id (:id item)
|
||||
blocked? (:blocked item)
|
||||
hidden? (:hidden item)
|
||||
@@ -264,21 +246,13 @@
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_]
|
||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
||||
(-> q
|
||||
(assoc :enter (conj enter id))
|
||||
(assoc :leave (disj leave id)))))
|
||||
(schedule-sidebar-hover-flush)))
|
||||
(st/emit! (dw/highlight-shape id))))
|
||||
|
||||
on-pointer-leave
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn [_]
|
||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
||||
(-> q
|
||||
(assoc :enter (disj enter id))
|
||||
(assoc :leave (conj leave id)))))
|
||||
(schedule-sidebar-hover-flush)))
|
||||
(st/emit! (dw/dehighlight-shape id))))
|
||||
|
||||
on-context-menu
|
||||
(mf/use-fn
|
||||
@@ -364,18 +338,14 @@
|
||||
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||
|
||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
||||
|
||||
;; Lazy loading of child elements via IntersectionObserver
|
||||
children-count* (mf/use-state 0)
|
||||
children-count (deref children-count*)
|
||||
lazy-ref (mf/use-ref nil)
|
||||
observer-var (mf/use-var nil)
|
||||
chunk-size 50]
|
||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))]
|
||||
|
||||
(mf/with-effect [selected? selected]
|
||||
(let [single? (= (count selected) 1)
|
||||
node (mf/ref-val ref)
|
||||
;; NOTE: Neither get-parent-at nor get-parent-with-selector
|
||||
;; work if the component template changes, so we need to
|
||||
;; seek for an alternate solution. Maybe use-context?
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
parent-node (dom/get-parent-at node 2)
|
||||
first-child-node (dom/get-first-child parent-node)
|
||||
@@ -393,62 +363,6 @@
|
||||
#(when (some? subid)
|
||||
(rx/dispose! subid))))
|
||||
|
||||
;; Setup scroll-driven lazy loading when expanded
|
||||
;; and ensures selected children are loaded immediately
|
||||
(mf/with-effect [expanded? (:shapes item) selected]
|
||||
(let [shapes-vec (:shapes item)
|
||||
total (count shapes-vec)]
|
||||
(if expanded?
|
||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
||||
;; Find if any selected id is a direct child and get its render index
|
||||
selected-child-render-idx
|
||||
(when (and (> total chunk-size) (seq selected))
|
||||
(let [shapes-reversed (vec (reverse shapes-vec))]
|
||||
(some (fn [sel-id]
|
||||
(let [idx (.indexOf shapes-reversed sel-id)]
|
||||
(when (>= idx 0) idx)))
|
||||
selected)))
|
||||
;; Load at least enough to include the selected child plus extra
|
||||
;; for context (so it can be centered in the scroll view)
|
||||
min-count (if selected-child-render-idx
|
||||
(+ selected-child-render-idx chunk-size)
|
||||
chunk-size)
|
||||
current @children-count*
|
||||
new-count (min total (max current chunk-size min-count))]
|
||||
(reset! children-count* new-count))
|
||||
(reset! children-count* 0)))
|
||||
(fn []
|
||||
(when-let [obs ^js @observer-var]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
;; and (shapes item) to reconnect observer after shape changes
|
||||
(mf/with-effect [children-count expanded? (:shapes item)]
|
||||
(let [total (count (:shapes item))
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
lazy-node (mf/ref-val lazy-ref)]
|
||||
;; Disconnect previous observer
|
||||
(when-let [obs ^js @observer-var]
|
||||
(.disconnect obs)
|
||||
(reset! observer-var nil))
|
||||
;; Setup new observer if there are more children to load
|
||||
(when (and expanded?
|
||||
(< children-count total)
|
||||
scroll-node
|
||||
lazy-node)
|
||||
(let [cb (fn [entries]
|
||||
(when (and (seq entries)
|
||||
(.-isIntersecting (first entries)))
|
||||
;; Load next chunk when sentinel intersects
|
||||
(let [current @children-count*
|
||||
next-count (min total (+ current chunk-size))]
|
||||
(reset! children-count* next-count))))
|
||||
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
|
||||
(.observe observer lazy-node)
|
||||
(reset! observer-var observer)))))
|
||||
|
||||
[:& layer-item-inner
|
||||
{:ref dref
|
||||
:item item
|
||||
@@ -473,32 +387,24 @@
|
||||
:on-enable-drag enable-drag
|
||||
:on-disable-drag disable-drag
|
||||
:on-toggle-visibility toggle-visibility
|
||||
:on-toggle-blocking toggle-blocking
|
||||
:style style}
|
||||
:on-toggle-blocking toggle-blocking}
|
||||
|
||||
(when (and render-children?
|
||||
(:shapes item)
|
||||
expanded?)
|
||||
(when (and (:shapes item) expanded?)
|
||||
[:div {:class (stl/css-case
|
||||
:element-children true
|
||||
:parent-selected selected?
|
||||
:sticky-children parent-board?)
|
||||
:data-testid (dm/str "children-" id)}
|
||||
(let [all-children (reverse (d/enumerate (:shapes item)))
|
||||
visible (take children-count all-children)]
|
||||
(for [[index id] visible]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}])))
|
||||
(when (< children-count (count (:shapes item)))
|
||||
[:div {:ref lazy-ref
|
||||
:style {:min-height 1}}])])]))
|
||||
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
||||
(when-let [item (get objects id)]
|
||||
[:& layer-item
|
||||
{:item item
|
||||
:highlighted highlighted
|
||||
:selected selected
|
||||
:index index
|
||||
:objects objects
|
||||
:key (dm/str id)
|
||||
:sortable? sortable?
|
||||
:depth depth
|
||||
:parent-size parent-size
|
||||
:component-child? component-tree?}]))])]))
|
||||
|
||||
@@ -116,29 +116,13 @@
|
||||
(->> (dm/get-in grid-edition [edition :selected])
|
||||
(map #(dm/get-in objects [edition :layout-grid-cells %])))
|
||||
|
||||
shapes-with-children*
|
||||
(mf/use-state nil)
|
||||
|
||||
_ (mf/use-effect
|
||||
(mf/deps selected objects shapes)
|
||||
(fn []
|
||||
(reset! shapes-with-children* nil)
|
||||
(let [result
|
||||
(loop [queue (into #queue [] selected)
|
||||
visited selected]
|
||||
(if-let [id (peek queue)]
|
||||
(let [shape (get objects id)
|
||||
children (:shapes shape)]
|
||||
(if (seq children)
|
||||
(let [new-children (remove visited children)]
|
||||
(recur (into (pop queue) new-children)
|
||||
(into visited new-children)))
|
||||
(recur (pop queue) visited)))
|
||||
(sequence (keep (d/getf objects)) visited)))]
|
||||
(reset! shapes-with-children* result))))
|
||||
|
||||
shapes-with-children
|
||||
(deref shapes-with-children*)
|
||||
(mf/with-memo [selected objects shapes]
|
||||
(let [xform (comp (remove nil?)
|
||||
(mapcat #(cfh/get-children-ids objects %)))
|
||||
selected (into selected xform selected)]
|
||||
(sequence (keep (d/getf objects)) selected)))
|
||||
|
||||
|
||||
total-selected
|
||||
(count selected)]
|
||||
|
||||
@@ -346,19 +346,17 @@
|
||||
{:value (:id variant)
|
||||
:key (pr-str variant)
|
||||
:label (:name variant)})))
|
||||
variant-options (if (or (= font-variant-id :multiple) (= font-variant-id "mixed"))
|
||||
variant-options (if (= font-variant-id :multiple)
|
||||
(conj basic-variant-options
|
||||
{:value ""
|
||||
:key :multiple-variants
|
||||
:label "--"})
|
||||
basic-variant-options)
|
||||
font-variant-value (attr->string font-variant-id)
|
||||
font-variant-value (if (= font-variant-value "mixed") "" font-variant-value)]
|
||||
basic-variant-options)]
|
||||
|
||||
;; TODO Add disabled mode
|
||||
[:& select
|
||||
{:class (stl/css :font-variant-select)
|
||||
:default-value font-variant-value
|
||||
:default-value (attr->string font-variant-id)
|
||||
:options variant-options
|
||||
:on-change on-font-variant-change
|
||||
:on-blur on-blur}])]]]))
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
@@ -23,11 +22,9 @@
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.badge :refer [badge-notification]]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as timers]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -55,37 +52,17 @@
|
||||
refs/workspace-data
|
||||
=))
|
||||
|
||||
|
||||
|
||||
;; --- Page Item
|
||||
|
||||
(mf/defc page-item
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [page index deletable? selected? editing? hovering? current-page-id]}]
|
||||
[{:keys [page index deletable? selected? editing? hovering?]}]
|
||||
(let [input-ref (mf/use-ref)
|
||||
id (:id page)
|
||||
delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
|
||||
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
|
||||
read-only? (mf/use-ctx ctx/workspace-read-only?)
|
||||
|
||||
on-click
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
(fn []
|
||||
;; For the wasm renderer, apply a blur effect to the viewport canvas
|
||||
;; when we navigate to a different page.
|
||||
(if (and (features/active-feature? @st/state "render-wasm/v1")
|
||||
(not= id current-page-id))
|
||||
(do
|
||||
(wasm.api/capture-canvas-pixels)
|
||||
(wasm.api/apply-canvas-blur)
|
||||
;; NOTE: it seems we need two RAF so the blur is actually applied and visible
|
||||
;; in the canvas :(
|
||||
(timers/raf
|
||||
(fn []
|
||||
(timers/raf navigate-fn))))
|
||||
(navigate-fn))))
|
||||
|
||||
on-delete
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
@@ -178,7 +155,7 @@
|
||||
:selected selected?)
|
||||
:data-testid (dm/str "page-" id)
|
||||
:tab-index "0"
|
||||
:on-click on-click
|
||||
:on-click navigate-fn
|
||||
:on-double-click on-double-click
|
||||
:on-context-menu on-context-menu}
|
||||
[:div {:class (stl/css :page-icon)}
|
||||
@@ -205,13 +182,12 @@
|
||||
|
||||
(mf/defc page-item-wrapper
|
||||
{::mf/wrap-props false}
|
||||
[{:keys [page-id index deletable? selected? editing? current-page-id]}]
|
||||
[{:keys [page-id index deletable? selected? editing?]}]
|
||||
(let [page-ref (mf/with-memo [page-id]
|
||||
(make-page-ref page-id))
|
||||
page (mf/deref page-ref)]
|
||||
[:& page-item {:page page
|
||||
:index index
|
||||
:current-page-id current-page-id
|
||||
:deletable? deletable?
|
||||
:selected? selected?
|
||||
:editing? editing?}]))
|
||||
@@ -234,7 +210,6 @@
|
||||
:deletable? deletable?
|
||||
:editing? (= page-id editing-page-id)
|
||||
:selected? (= page-id current-page-id)
|
||||
:current-page-id current-page-id
|
||||
:key page-id}])]]))
|
||||
|
||||
;; --- Sitemap Toolbox
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.color :as cl]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.tinycolor :as tinycolor]
|
||||
@@ -26,6 +27,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Color Inputs -------------------------------------------------------------
|
||||
@@ -51,12 +53,15 @@
|
||||
;; Both variants provide identical color-picker and text-input behavior, but
|
||||
;; differ in how they persist the value within the form’s nested structure.
|
||||
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(m/validate cto/token-name-ref token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; --- Font Picker Inputs -------------------------------------------------------
|
||||
@@ -50,9 +51,13 @@
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(m/validate cto/token-name-ref token-name))
|
||||
|
||||
token
|
||||
{:value (cto/split-font-family value)
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.files.tokens :as cft]
|
||||
[app.common.types.token :as cto]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.style-dictionary :as sd]
|
||||
[app.main.data.workspace.tokens.format :as dwtf]
|
||||
@@ -18,6 +19,7 @@
|
||||
[app.util.i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[malli.core :as m]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
;; -----------------------------------------------------------------------------
|
||||
@@ -140,9 +142,13 @@
|
||||
|
||||
(defn- resolve-value
|
||||
[tokens prev-token token-name value]
|
||||
(let [token
|
||||
(let [valid-token-name?
|
||||
(and (string? token-name)
|
||||
(m/validate cto/token-name-ref token-name))
|
||||
|
||||
token
|
||||
{:value value
|
||||
:name (if (str/blank? token-name)
|
||||
:name (if (or (not valid-token-name?) (str/blank? token-name))
|
||||
"__PENPOT__TOKEN__NAME__PLACEHOLDER__"
|
||||
token-name)}
|
||||
tokens
|
||||
|
||||
@@ -144,7 +144,7 @@
|
||||
modifiers (hooks/use-equal-memo modifiers)
|
||||
shapes (hooks/use-equal-memo shapes)]
|
||||
|
||||
[:g.outlines.blurrable
|
||||
[:g.outlines
|
||||
[:& shape-outlines-render {:shapes shapes
|
||||
:zoom zoom
|
||||
:modifiers modifiers}]]))
|
||||
|
||||
@@ -252,7 +252,7 @@
|
||||
edition (mf/deref refs/selected-edition)
|
||||
grid-edition? (ctl/grid-layout? objects edition)]
|
||||
|
||||
[:g.frame-titles.blurrable
|
||||
[:g.frame-titles
|
||||
(for [{:keys [id parent-id] :as shape} shapes]
|
||||
(when (and
|
||||
(not= id uuid/zero)
|
||||
|
||||
@@ -312,11 +312,6 @@
|
||||
(js/console.error "Error initializing canvas context:" e)
|
||||
false))]
|
||||
(reset! canvas-init? init?)
|
||||
(when init?
|
||||
;; Restore previous canvas pixels immediately after context initialization
|
||||
;; This happens before initialize-viewport is called
|
||||
(wasm.api/apply-canvas-blur)
|
||||
(wasm.api/restore-previous-canvas-pixels))
|
||||
(when-not init?
|
||||
(js/alert "WebGL not supported")
|
||||
(st/emit! (dcm/go-to-dashboard-recent))))))))
|
||||
@@ -345,7 +340,6 @@
|
||||
|
||||
(mf/with-effect [@canvas-init? zoom vbox background]
|
||||
(when (and @canvas-init? (not @initialized?))
|
||||
(wasm.api/clear-canvas-pixels)
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox background)
|
||||
(reset! initialized? true)))
|
||||
|
||||
@@ -424,7 +418,6 @@
|
||||
:xmlnsXlink "http://www.w3.org/1999/xlink"
|
||||
:preserveAspectRatio "xMidYMid meet"
|
||||
:key (str "viewport" page-id)
|
||||
:id "viewport-controls"
|
||||
:view-box (utils/format-viewbox vbox)
|
||||
:ref on-viewport-ref
|
||||
:class (dm/str @cursor (when drawing-tool " drawing") " " (stl/css :viewport-controls))
|
||||
@@ -474,7 +467,7 @@
|
||||
:zoom zoom}]
|
||||
|
||||
(when (ctl/any-layout? outlined-frame)
|
||||
[:g.ghost-outline.blurrable
|
||||
[:g.ghost-outline
|
||||
[:& outline/shape-outlines
|
||||
{:objects base-objects
|
||||
:selected selected
|
||||
|
||||
@@ -805,8 +805,7 @@
|
||||
(u/display-not-valid :resize "Plugin doesn't have 'content:write' permission")
|
||||
|
||||
:else
|
||||
(prn "resize" width height)
|
||||
#_(st/emit! (dw/update-dimensions [id] :width width)
|
||||
(st/emit! (dw/update-dimensions [id] :width width)
|
||||
(dw/update-dimensions [id] :height height))))
|
||||
|
||||
:rotate
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
@@ -22,15 +21,14 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.render-wasm :as drw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :as render]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.shapes.text]
|
||||
[app.main.worker :as mw]
|
||||
[app.render-wasm.api.fonts :as f]
|
||||
[app.render-wasm.api.shapes :as shapes]
|
||||
[app.render-wasm.api.texts :as t]
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[app.render-wasm.deserializers :as dr]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
@@ -39,6 +37,7 @@
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.serializers.color :as sr-clr]
|
||||
[app.render-wasm.svg-filters :as svg-filters]
|
||||
;; FIXME: rename; confunsing name
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
@@ -69,25 +68,12 @@
|
||||
(def ^:const DEBOUNCE_DELAY_MS 100)
|
||||
(def ^:const THROTTLE_DELAY_MS 10)
|
||||
|
||||
;; Number of shapes to process before yielding to browser
|
||||
(def ^:const SHAPES_CHUNK_SIZE 100)
|
||||
;; Threshold below which we use synchronous processing (no chunking overhead)
|
||||
(def ^:const ASYNC_THRESHOLD 100)
|
||||
|
||||
(def dpr
|
||||
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
||||
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||
[]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(js/requestAnimationFrame (fn [_] (resolve nil))))))
|
||||
|
||||
;; Based on app.main.render/object-svg
|
||||
(mf/defc object-svg
|
||||
{::mf/props :obj}
|
||||
@@ -134,70 +120,28 @@
|
||||
(aget buffer 3))
|
||||
(set! wasm/internal-frame-id nil))))
|
||||
|
||||
(defn render-preview!
|
||||
"Render a lightweight preview without tile caching.
|
||||
Used during progressive loading for fast feedback."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_preview")))
|
||||
|
||||
|
||||
(defonce pending-render (atom false))
|
||||
(defonce shapes-loading? (atom false))
|
||||
(defonce deferred-render? (atom false))
|
||||
|
||||
(defn- register-deferred-render!
|
||||
[]
|
||||
(reset! deferred-render? true))
|
||||
|
||||
(defn request-render
|
||||
[_requester]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(if @shapes-loading?
|
||||
(register-deferred-render!)
|
||||
(when-not @pending-render
|
||||
(reset! pending-render true)
|
||||
(let [frame-id
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(set! wasm/internal-frame-id nil)
|
||||
(render ts)))]
|
||||
(set! wasm/internal-frame-id frame-id))))))
|
||||
|
||||
(defn- begin-shapes-loading!
|
||||
[]
|
||||
(reset! shapes-loading? true)
|
||||
(let [frame-id wasm/internal-frame-id
|
||||
was-pending @pending-render]
|
||||
(when frame-id
|
||||
(js/cancelAnimationFrame frame-id)
|
||||
(set! wasm/internal-frame-id nil))
|
||||
(reset! pending-render false)
|
||||
(reset! deferred-render? was-pending)))
|
||||
|
||||
(defn- end-shapes-loading!
|
||||
[]
|
||||
(let [was-loading (compare-and-set! shapes-loading? true false)]
|
||||
(reset! deferred-render? false)
|
||||
;; Always trigger a render after loading completes
|
||||
;; This ensures shapes are displayed even if no deferred render was requested
|
||||
(when was-loading
|
||||
(request-render "set-objects:flush"))))
|
||||
(when (and wasm/context-initialized? (not @pending-render) (not @wasm/context-lost?))
|
||||
(reset! pending-render true)
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(render ts)))))
|
||||
|
||||
(declare get-text-dimensions)
|
||||
|
||||
(defn update-text-rect!
|
||||
[id]
|
||||
(when wasm/context-initialized?
|
||||
(let [dimensions (get-text-dimensions id)
|
||||
page-id (:current-page-id @st/state)]
|
||||
;;(prn ">update-text-rect!" id dimensions)
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id page-id
|
||||
:shape-id id
|
||||
:dimensions dimensions}))))
|
||||
(mw/emit!
|
||||
{:cmd :index/update-text-rect
|
||||
:page-id (:current-page-id @st/state)
|
||||
:shape-id id
|
||||
:dimensions (get-text-dimensions id)})))
|
||||
|
||||
|
||||
(defn- ensure-text-content
|
||||
@@ -335,6 +279,30 @@
|
||||
[string]
|
||||
(+ (count string) 1))
|
||||
|
||||
(defn- create-webgl-texture-from-image
|
||||
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
|
||||
[gl image-element]
|
||||
(let [texture (.createTexture ^js gl)]
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||
texture))
|
||||
|
||||
(defn- get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
|
||||
(when gl-obj
|
||||
;; Get the current WebGL context from Emscripten
|
||||
;; The GL object has a currentContext property that contains the context handle
|
||||
(let [current-ctx (.-currentContext ^js gl-obj)]
|
||||
(when current-ctx
|
||||
(.-GLctx ^js current-ctx)))))))
|
||||
|
||||
(defn- get-texture-id-for-gl-object
|
||||
"Registers a WebGL texture with Emscripten's GL object system and returns its ID"
|
||||
@@ -364,8 +332,8 @@
|
||||
(->> (retrieve-image url)
|
||||
(rx/map
|
||||
(fn [img]
|
||||
(when-let [gl (webgl/get-webgl-context)]
|
||||
(let [texture (webgl/create-webgl-texture-from-image gl img)
|
||||
(when-let [gl (get-webgl-context)]
|
||||
(let [texture (create-webgl-texture-from-image gl img)
|
||||
texture-id (get-texture-id-for-gl-object texture)
|
||||
width (.-width ^js img)
|
||||
height (.-height ^js img)
|
||||
@@ -951,12 +919,24 @@
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
parent-id (get shape :parent-id)
|
||||
masked (get shape :masked-group)
|
||||
selrect (get shape :selrect)
|
||||
constraint-h (get shape :constraints-h)
|
||||
constraint-v (get shape :constraints-v)
|
||||
clip-content (if (= type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
rotation (get shape :rotation)
|
||||
transform (get shape :transform)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
blend-mode (get shape :blend-mode)
|
||||
opacity (get shape :opacity)
|
||||
hidden (get shape :hidden)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
@@ -965,12 +945,22 @@
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)]
|
||||
shadows (get shape :shadow)
|
||||
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
|
||||
|
||||
(shapes/set-shape-base-props shape)
|
||||
(use-shape id)
|
||||
(set-parent-id parent-id)
|
||||
(set-shape-type type)
|
||||
(set-shape-clip-content clip-content)
|
||||
(set-shape-constraints constraint-h constraint-v)
|
||||
|
||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||
(set-shape-rotation rotation)
|
||||
(set-shape-transform transform)
|
||||
(set-shape-blend-mode blend-mode)
|
||||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
(set-shape-children children)
|
||||
(set-shape-corners corners)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
@@ -989,7 +979,7 @@
|
||||
(set-shape-grow-type grow-type))
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(set-shape-selrect selrect)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
@@ -1045,143 +1035,29 @@
|
||||
(let [{:keys [thumbnails full]} (set-object shape)]
|
||||
(process-pending [shape] thumbnails full noop-fn)))
|
||||
|
||||
(defn- process-shapes-chunk
|
||||
"Process a chunk of shapes synchronously, returning accumulated pending operations.
|
||||
Returns {:thumbnails [...] :full [...] :next-index n}"
|
||||
[shapes start-index chunk-size thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
end-index (min total (+ start-index chunk-size))]
|
||||
(loop [index start-index
|
||||
t-acc thumbnails-acc
|
||||
f-acc full-acc]
|
||||
(if (< index end-index)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into t-acc thumbnails)
|
||||
(into f-acc full)))
|
||||
{:thumbnails t-acc
|
||||
:full f-acc
|
||||
:next-index end-index}))))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in chunks, yielding to the browser between chunks.
|
||||
Returns a promise that resolves when all shapes are processed.
|
||||
|
||||
Renders a preview only periodically during loading to show progress,
|
||||
then does a full tile-based render at the end."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE))
|
||||
;; Render at 25%, 50%, 75% of loading
|
||||
render-at-chunks (set [(mth/floor (* total-chunks 0.25))
|
||||
(mth/floor (* total-chunks 0.5))
|
||||
(mth/floor (* total-chunks 0.75))])]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count]
|
||||
(if (< index total-shapes)
|
||||
;; Process one chunk
|
||||
(let [{:keys [thumbnails full next-index]}
|
||||
(process-shapes-chunk shapes index SHAPES_CHUNK_SIZE
|
||||
thumbnails-acc full-acc)
|
||||
new-chunk-count (inc chunk-count)]
|
||||
;; Only render at specific progress milestones
|
||||
(when (contains? render-at-chunks new-chunk-count)
|
||||
(render-preview!))
|
||||
|
||||
;; Yield to browser, then continue with next chunk
|
||||
(-> (yield-to-browser)
|
||||
(p/then (fn [_]
|
||||
(process-next-chunk next-index thumbnails full new-chunk-count)))))
|
||||
;; All chunks done - finalize
|
||||
(do
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails-acc full-acc noop-fn
|
||||
(fn []
|
||||
(end-shapes-loading!)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
|
||||
(resolve nil))))))]
|
||||
(process-next-chunk 0 [] [] 0))))))
|
||||
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
|
||||
|
||||
(defn- shapes-in-tree-order
|
||||
"Returns shapes sorted in tree order (parents before children).
|
||||
This ensures parent shapes are processed before their children,
|
||||
maintaining proper shape reference consistency in WASM."
|
||||
[objects]
|
||||
;; Get IDs in tree order starting from root (uuid/zero)
|
||||
;; If root doesn't exist (e.g., filtered thumbnail data), fall back to
|
||||
;; finding top-level shapes (those without a parent in objects) and
|
||||
;; traversing from there.
|
||||
(if (contains? objects uuid/zero)
|
||||
;; Normal case: traverse from root
|
||||
(let [ordered-ids (cfh/get-children-ids-with-self objects uuid/zero)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
ordered-ids))
|
||||
;; Fallback for filtered data (thumbnails): find top-level shapes and traverse
|
||||
(let [;; Find shapes whose parent is not in the objects map (top-level in this subset)
|
||||
top-level-ids (->> (vals objects)
|
||||
(filter (fn [shape]
|
||||
(not (contains? objects (:parent-id shape)))))
|
||||
(map :id))
|
||||
;; Get all children in order for each top-level shape
|
||||
all-ordered-ids (into []
|
||||
(mapcat #(cfh/get-children-ids-with-self objects %))
|
||||
top-level-ids)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
all-ordered-ids))))
|
||||
|
||||
(defn set-objects
|
||||
"Set all shape objects for rendering.
|
||||
|
||||
Shapes are processed in tree order (parents before children)
|
||||
to maintain proper shape reference consistency in WASM."
|
||||
([objects]
|
||||
(set-objects objects nil))
|
||||
([objects render-callback]
|
||||
(perf/begin-measure "set-objects")
|
||||
(let [shapes (shapes-in-tree-order objects)
|
||||
total-shapes (count shapes)]
|
||||
(if (< total-shapes ASYNC_THRESHOLD)
|
||||
(set-objects-sync shapes render-callback)
|
||||
(do
|
||||
(begin-shapes-loading!)
|
||||
(try
|
||||
(-> (set-objects-async shapes render-callback)
|
||||
(p/catch (fn [error]
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error))))
|
||||
(catch :default error
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error)
|
||||
(throw error)))
|
||||
nil)))))
|
||||
(let [shapes (into [] (vals objects))
|
||||
total-shapes (count shapes)
|
||||
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(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
|
||||
[]
|
||||
@@ -1360,8 +1236,7 @@
|
||||
(dom/prevent-default event)
|
||||
(reset! wasm/context-lost? true)
|
||||
(log/warn :hint "WebGL context lost")
|
||||
(ex/raise :type :webgl-context-lost
|
||||
:hint "WebGL context lost"))
|
||||
(st/emit! (drw/context-lost)))
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
@@ -1508,9 +1383,8 @@
|
||||
all-children
|
||||
(->> ids
|
||||
(mapcat #(cfh/get-children-with-self objects %)))]
|
||||
|
||||
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
|
||||
(run! set-object all-children)
|
||||
(run! (partial set-object objects) all-children)
|
||||
|
||||
(let [content (-> (calculate-bool* bool-type ids)
|
||||
(path.impl/path-data))]
|
||||
@@ -1573,13 +1447,6 @@
|
||||
|
||||
result)))
|
||||
|
||||
(defn apply-canvas-blur
|
||||
[]
|
||||
(when wasm/canvas (dom/set-style! wasm/canvas "filter" "blur(4px)"))
|
||||
(let [controls-to-blur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
||||
(run! #(dom/set-style! % "filter" "blur(4px)") controls-to-blur)))
|
||||
|
||||
|
||||
(defn init-wasm-module
|
||||
[module]
|
||||
(let [default-fn (unchecked-get module "default")
|
||||
@@ -1601,8 +1468,3 @@
|
||||
(js/console.error cause)
|
||||
(p/resolved false)))))
|
||||
(p/resolved false))))
|
||||
|
||||
;; Re-export public WebGL functions
|
||||
(def capture-canvas-pixels webgl/capture-canvas-pixels)
|
||||
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
|
||||
(def clear-canvas-pixels webgl/clear-canvas-pixels)
|
||||
|
||||
@@ -124,25 +124,19 @@
|
||||
|
||||
true))
|
||||
|
||||
(def fetching (atom #{}))
|
||||
|
||||
(defn- fetch-font
|
||||
[shape-id font-data font-url emoji? fallback?]
|
||||
(when-not (contains? @fetching font-url)
|
||||
(swap! fetching conj font-url)
|
||||
{:key font-url
|
||||
:callback #(->> (http/send! {:method :get
|
||||
:uri font-url
|
||||
:response-type :buffer})
|
||||
(rx/map (fn [{:keys [body]}]
|
||||
(swap! fetching disj font-url)
|
||||
(store-font-buffer shape-id font-data body emoji? fallback?)))
|
||||
(rx/catch (fn [cause]
|
||||
(swap! fetching disj font-url)
|
||||
(log/error :hint "Could not fetch font"
|
||||
:font-url font-url
|
||||
:cause cause)
|
||||
(rx/empty))))}))
|
||||
{:key font-url
|
||||
:callback #(->> (http/send! {:method :get
|
||||
:uri font-url
|
||||
:response-type :buffer})
|
||||
(rx/map (fn [{:keys [body]}]
|
||||
(store-font-buffer shape-id font-data body emoji? fallback?)))
|
||||
(rx/catch (fn [cause]
|
||||
(log/error :hint "Could not fetch font"
|
||||
:font-url font-url
|
||||
:cause cause)
|
||||
(rx/empty))))})
|
||||
|
||||
(defn- google-font-ttf-url
|
||||
[font-id font-variant-id font-weight font-style]
|
||||
|
||||
@@ -1,193 +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.render-wasm.api.shapes
|
||||
"Batched shape property serialization for improved WASM performance.
|
||||
|
||||
This module provides a single WASM call to set all base shape properties,
|
||||
replacing multiple individual calls (use_shape, set_parent, set_shape_type,
|
||||
etc.) with one batched operation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.wasm :as wasm]))
|
||||
|
||||
;; Binary layout constants matching Rust implementation:
|
||||
;;
|
||||
;; | Offset | Size | Field | Type |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | 0 | 16 | id | UUID (4 × u32 LE) |
|
||||
;; | 16 | 16 | parent_id | UUID (4 × u32 LE) |
|
||||
;; | 32 | 1 | shape_type | u8 |
|
||||
;; | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
|
||||
;; | 34 | 1 | blend_mode | u8 |
|
||||
;; | 35 | 1 | constraint_h | u8 (0xFF = None) |
|
||||
;; | 36 | 1 | constraint_v | u8 (0xFF = None) |
|
||||
;; | 37 | 3 | padding | - |
|
||||
;; | 40 | 4 | opacity | f32 LE |
|
||||
;; | 44 | 4 | rotation | f32 LE |
|
||||
;; | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
|
||||
;; | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
|
||||
;; | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | Total | 104 | | |
|
||||
|
||||
(def ^:const BASE-PROPS-SIZE 104)
|
||||
(def ^:const FLAG-CLIP-CONTENT 0x01)
|
||||
(def ^:const FLAG-HIDDEN 0x02)
|
||||
(def ^:const CONSTRAINT-NONE 0xFF)
|
||||
|
||||
(defn- write-uuid-to-heap
|
||||
"Write a UUID to the heap at the given byte offset using DataView."
|
||||
[dview offset id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(.setUint32 dview offset (aget buffer 0) true)
|
||||
(.setUint32 dview (+ offset 4) (aget buffer 1) true)
|
||||
(.setUint32 dview (+ offset 8) (aget buffer 2) true)
|
||||
(.setUint32 dview (+ offset 12) (aget buffer 3) true)))
|
||||
|
||||
(defn- serialize-transform
|
||||
"Extract transform matrix values, defaulting to identity matrix."
|
||||
[transform]
|
||||
(if (some? transform)
|
||||
[(dm/get-prop transform :a)
|
||||
(dm/get-prop transform :b)
|
||||
(dm/get-prop transform :c)
|
||||
(dm/get-prop transform :d)
|
||||
(dm/get-prop transform :e)
|
||||
(dm/get-prop transform :f)]
|
||||
[1.0 0.0 0.0 1.0 0.0 0.0])) ; identity matrix
|
||||
|
||||
(defn- serialize-selrect
|
||||
"Extract selrect values."
|
||||
[selrect]
|
||||
(if (some? selrect)
|
||||
[(dm/get-prop selrect :x1)
|
||||
(dm/get-prop selrect :y1)
|
||||
(dm/get-prop selrect :x2)
|
||||
(dm/get-prop selrect :y2)]
|
||||
[0.0 0.0 0.0 0.0]))
|
||||
|
||||
(defn set-shape-base-props
|
||||
"Set all base shape properties in a single WASM call.
|
||||
|
||||
This replaces the following individual calls:
|
||||
- use-shape
|
||||
- set-parent-id
|
||||
- set-shape-type
|
||||
- set-shape-clip-content
|
||||
- set-shape-rotation
|
||||
- set-shape-transform
|
||||
- set-shape-blend-mode
|
||||
- set-shape-opacity
|
||||
- set-shape-hidden
|
||||
- set-shape-selrect
|
||||
- set-shape-corners
|
||||
- set-shape-constraints (clear + h + v)
|
||||
|
||||
Returns nil."
|
||||
[shape]
|
||||
(when wasm/context-initialized?
|
||||
(let [id (dm/get-prop shape :id)
|
||||
parent-id (get shape :parent-id)
|
||||
shape-type (dm/get-prop shape :type)
|
||||
|
||||
clip-content (if (= shape-type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
hidden (get shape :hidden false)
|
||||
|
||||
flags (cond-> 0
|
||||
clip-content (bit-or FLAG-CLIP-CONTENT)
|
||||
hidden (bit-or FLAG-HIDDEN))
|
||||
|
||||
blend-mode (sr/translate-blend-mode (get shape :blend-mode))
|
||||
constraint-h (let [c (get shape :constraints-h)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-h c)
|
||||
CONSTRAINT-NONE))
|
||||
constraint-v (let [c (get shape :constraints-v)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-v c)
|
||||
CONSTRAINT-NONE))
|
||||
|
||||
opacity (d/nilv (get shape :opacity) 1.0)
|
||||
rotation (d/nilv (get shape :rotation) 0.0)
|
||||
|
||||
;; Transform matrix
|
||||
[ta tb tc td te tf] (serialize-transform (get shape :transform))
|
||||
|
||||
;; Selrect
|
||||
selrect (get shape :selrect)
|
||||
[sx1 sy1 sx2 sy2] (serialize-selrect selrect)
|
||||
|
||||
;; Corners
|
||||
r1 (d/nilv (get shape :r1) 0.0)
|
||||
r2 (d/nilv (get shape :r2) 0.0)
|
||||
r3 (d/nilv (get shape :r3) 0.0)
|
||||
r4 (d/nilv (get shape :r4) 0.0)
|
||||
|
||||
;; Allocate buffer and get DataView
|
||||
offset (mem/alloc BASE-PROPS-SIZE)
|
||||
heap (mem/get-heap-u8)
|
||||
dview (js/DataView. (.-buffer heap))]
|
||||
|
||||
;; Write id (offset 0, 16 bytes)
|
||||
(write-uuid-to-heap dview offset id)
|
||||
|
||||
;; Write parent_id (offset 16, 16 bytes)
|
||||
(write-uuid-to-heap dview (+ offset 16) (d/nilv parent-id uuid/zero))
|
||||
|
||||
;; Write shape_type (offset 32, 1 byte)
|
||||
(.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type))
|
||||
|
||||
;; Write flags (offset 33, 1 byte)
|
||||
(.setUint8 dview (+ offset 33) flags)
|
||||
|
||||
;; Write blend_mode (offset 34, 1 byte)
|
||||
(.setUint8 dview (+ offset 34) blend-mode)
|
||||
|
||||
;; Write constraint_h (offset 35, 1 byte)
|
||||
(.setUint8 dview (+ offset 35) constraint-h)
|
||||
|
||||
;; Write constraint_v (offset 36, 1 byte)
|
||||
(.setUint8 dview (+ offset 36) constraint-v)
|
||||
|
||||
;; Padding at offset 37-39 (already zero from alloc)
|
||||
|
||||
;; Write opacity (offset 40, f32)
|
||||
(.setFloat32 dview (+ offset 40) opacity true)
|
||||
|
||||
;; Write rotation (offset 44, f32)
|
||||
(.setFloat32 dview (+ offset 44) rotation true)
|
||||
|
||||
;; Write transform matrix (offset 48, 6 × f32)
|
||||
(.setFloat32 dview (+ offset 48) ta true)
|
||||
(.setFloat32 dview (+ offset 52) tb true)
|
||||
(.setFloat32 dview (+ offset 56) tc true)
|
||||
(.setFloat32 dview (+ offset 60) td true)
|
||||
(.setFloat32 dview (+ offset 64) te true)
|
||||
(.setFloat32 dview (+ offset 68) tf true)
|
||||
|
||||
;; Write selrect (offset 72, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 72) sx1 true)
|
||||
(.setFloat32 dview (+ offset 76) sy1 true)
|
||||
(.setFloat32 dview (+ offset 80) sx2 true)
|
||||
(.setFloat32 dview (+ offset 84) sy2 true)
|
||||
|
||||
;; Write corners (offset 88, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 88) r1 true)
|
||||
(.setFloat32 dview (+ offset 92) r2 true)
|
||||
(.setFloat32 dview (+ offset 96) r3 true)
|
||||
(.setFloat32 dview (+ offset 100) r4 true)
|
||||
|
||||
(h/call wasm/internal-module "_set_shape_base_props")
|
||||
|
||||
nil)))
|
||||
@@ -1,168 +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.render-wasm.api.webgl
|
||||
"WebGL utilities for pixel capture and rendering"
|
||||
(:require
|
||||
[app.common.logging :as log]
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.util.dom :as dom]))
|
||||
|
||||
(defn get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
|
||||
(when gl-obj
|
||||
;; Get the current WebGL context from Emscripten
|
||||
;; The GL object has a currentContext property that contains the context handle
|
||||
(let [current-ctx (.-currentContext ^js gl-obj)]
|
||||
(when current-ctx
|
||||
(.-GLctx ^js current-ctx)))))))
|
||||
|
||||
(defn create-webgl-texture-from-image
|
||||
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
|
||||
[gl image-element]
|
||||
(let [texture (.createTexture ^js gl)]
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||
texture))
|
||||
|
||||
;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
|
||||
(defn- draw-imagedata-to-webgl
|
||||
"Draws ImageData to a WebGL2 context by creating a texture"
|
||||
[gl image-data]
|
||||
(let [width (.-width ^js image-data)
|
||||
height (.-height ^js image-data)
|
||||
texture (.createTexture ^js gl)]
|
||||
;; Bind texture and set parameters
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data)
|
||||
|
||||
;; Set up viewport
|
||||
(.viewport ^js gl 0 0 width height)
|
||||
|
||||
;; Vertex & Fragment shaders
|
||||
;; Since we are only calling this function once (on page switch), we don't need
|
||||
;; to cache the compiled shaders somewhere else (cannot be reused in a differen context).
|
||||
(let [vertex-shader-source "#version 300 es
|
||||
in vec2 a_position;
|
||||
in vec2 a_texCoord;
|
||||
out vec2 v_texCoord;
|
||||
void main() {
|
||||
gl_Position = vec4(a_position, 0.0, 1.0);
|
||||
v_texCoord = a_texCoord;
|
||||
}"
|
||||
fragment-shader-source "#version 300 es
|
||||
precision highp float;
|
||||
in vec2 v_texCoord;
|
||||
uniform sampler2D u_texture;
|
||||
out vec4 fragColor;
|
||||
void main() {
|
||||
fragColor = texture(u_texture, v_texCoord);
|
||||
}"
|
||||
vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl))
|
||||
fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl))
|
||||
program (.createProgram ^js gl)]
|
||||
(.shaderSource ^js gl vertex-shader vertex-shader-source)
|
||||
(.compileShader ^js gl vertex-shader)
|
||||
(when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl))
|
||||
(log/error :hint "Vertex shader compilation failed"
|
||||
:log (.getShaderInfoLog ^js gl vertex-shader)))
|
||||
|
||||
(.shaderSource ^js gl fragment-shader fragment-shader-source)
|
||||
(.compileShader ^js gl fragment-shader)
|
||||
(when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl))
|
||||
(log/error :hint "Fragment shader compilation failed"
|
||||
:log (.getShaderInfoLog ^js gl fragment-shader)))
|
||||
|
||||
(.attachShader ^js gl program vertex-shader)
|
||||
(.attachShader ^js gl program fragment-shader)
|
||||
(.linkProgram ^js gl program)
|
||||
|
||||
(if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl))
|
||||
(do
|
||||
(.useProgram ^js gl program)
|
||||
|
||||
;; Create full-screen quad vertices (normalized device coordinates)
|
||||
(let [position-location (.getAttribLocation ^js gl program "a_position")
|
||||
texcoord-location (.getAttribLocation ^js gl program "a_texCoord")
|
||||
position-buffer (.createBuffer ^js gl)
|
||||
texcoord-buffer (.createBuffer ^js gl)
|
||||
positions #js [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0]
|
||||
texcoords #js [0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0]]
|
||||
;; Set up position buffer
|
||||
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer)
|
||||
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl))
|
||||
(.enableVertexAttribArray ^js gl position-location)
|
||||
(.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0)
|
||||
;; Set up texcoord buffer
|
||||
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer)
|
||||
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl))
|
||||
(.enableVertexAttribArray ^js gl texcoord-location)
|
||||
(.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0)
|
||||
;; Set texture uniform
|
||||
(.activeTexture ^js gl (.-TEXTURE0 ^js gl))
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||
(let [texture-location (.getUniformLocation ^js gl program "u_texture")]
|
||||
(.uniform1i ^js gl texture-location 0))
|
||||
|
||||
;; draw
|
||||
(.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6)
|
||||
|
||||
;; cleanup
|
||||
(.deleteBuffer ^js gl position-buffer)
|
||||
(.deleteBuffer ^js gl texcoord-buffer)
|
||||
(.deleteShader ^js gl vertex-shader)
|
||||
(.deleteShader ^js gl fragment-shader)
|
||||
(.deleteProgram ^js gl program)))
|
||||
(log/error :hint "Program linking failed"
|
||||
:log (.getProgramInfoLog ^js gl program)))
|
||||
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||
(.deleteTexture ^js gl texture))))
|
||||
|
||||
(defn restore-previous-canvas-pixels
|
||||
"Restores previous canvas pixels into the new canvas"
|
||||
[]
|
||||
(when-let [previous-canvas-pixels wasm/canvas-pixels]
|
||||
(when-let [gl wasm/gl-context]
|
||||
(draw-imagedata-to-webgl gl previous-canvas-pixels)
|
||||
(set! wasm/canvas-pixels nil))))
|
||||
|
||||
(defn clear-canvas-pixels
|
||||
[]
|
||||
(when wasm/canvas
|
||||
(let [context wasm/gl-context]
|
||||
(.clearColor ^js context 0 0 0 0.0)
|
||||
(.clear ^js context (.-COLOR_BUFFER_BIT ^js context))
|
||||
(.clear ^js context (.-DEPTH_BUFFER_BIT ^js context))
|
||||
(.clear ^js context (.-STENCIL_BUFFER_BIT ^js context)))
|
||||
(dom/set-style! wasm/canvas "filter" "none")
|
||||
(let [controls-to-unblur (dom/query-all (dom/get-element "viewport-controls") ".blurrable")]
|
||||
(run! #(dom/set-style! % "filter" "none") controls-to-unblur))
|
||||
(set! wasm/canvas-pixels nil)))
|
||||
|
||||
(defn capture-canvas-pixels
|
||||
"Captures the pixels of the viewport canvas"
|
||||
[]
|
||||
(when wasm/canvas
|
||||
(let [context wasm/gl-context
|
||||
width (.-width wasm/canvas)
|
||||
height (.-height wasm/canvas)
|
||||
buffer (js/Uint8ClampedArray. (* width height 4))
|
||||
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
|
||||
image-data (js/ImageData. buffer width height)]
|
||||
(set! wasm/canvas-pixels image-data))))
|
||||
@@ -227,7 +227,7 @@
|
||||
:svg-attrs
|
||||
(do
|
||||
(api/set-shape-svg-attrs v)
|
||||
;; Always update fills/blur/shadow to clear previous state if filters disappear
|
||||
;; Always update fills/blur/shadow to clear previous state if filters disappear
|
||||
(api/set-shape-fills id (:fills shape) false)
|
||||
(api/set-shape-blur (:blur shape))
|
||||
(api/set-shape-shadows (:shadow shape)))
|
||||
@@ -397,18 +397,12 @@
|
||||
(next es))
|
||||
(throw (js/Error. "conj on a map takes map entries or seqables of map entries"))))))))
|
||||
|
||||
(def ^:private xf:without-id-and-type
|
||||
(remove (fn [kvpair]
|
||||
(let [k (key kvpair)]
|
||||
(or (= k :id)
|
||||
(= k :type))))))
|
||||
|
||||
(defn create-shape
|
||||
"Instanciate a shape from a map"
|
||||
[attrs]
|
||||
(ShapeProxy. (:id attrs)
|
||||
(:type attrs)
|
||||
(into {} xf:without-id-and-type attrs)))
|
||||
(dissoc attrs :id :type)))
|
||||
|
||||
(t/add-handlers!
|
||||
;; We only add a write handler, read handler uses the dynamic dispatch
|
||||
|
||||
@@ -12,8 +12,6 @@
|
||||
|
||||
;; Reference to the HTML canvas element.
|
||||
(defonce canvas nil)
|
||||
;; Reference to the captured pixels of the canvas (for page switching effect)
|
||||
(defonce canvas-pixels nil)
|
||||
|
||||
;; Reference to the Emscripten GL context wrapper.
|
||||
(defonce gl-context-handle nil)
|
||||
@@ -58,4 +56,3 @@
|
||||
:stroke-linecap shared/RawStrokeLineCap
|
||||
:stroke-linejoin shared/RawStrokeLineJoin
|
||||
:fill-rule shared/RawFillRule})
|
||||
|
||||
|
||||
@@ -23,15 +23,15 @@
|
||||
[node]
|
||||
(is-element node "br"))
|
||||
|
||||
(defn is-text-span-child
|
||||
(defn is-inline-child
|
||||
[node]
|
||||
(or (is-line-break node)
|
||||
(is-text-node node)))
|
||||
|
||||
(defn get-text-span-text
|
||||
(defn get-inline-text
|
||||
[element]
|
||||
(when-not (is-text-span-child (.-firstChild element))
|
||||
(throw (js/TypeError. "Invalid text span child")))
|
||||
(when-not (is-inline-child (.-firstChild element))
|
||||
(throw (js/TypeError. "Invalid inline child")))
|
||||
(if (is-line-break (.-firstChild element))
|
||||
""
|
||||
(.-textContent element)))
|
||||
@@ -54,7 +54,7 @@
|
||||
(assoc acc key (if (value-empty? value) (get defaults key) value))))
|
||||
{} attrs)))
|
||||
|
||||
(defn get-text-span-styles
|
||||
(defn get-inline-styles
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
||||
|
||||
@@ -66,18 +66,18 @@
|
||||
[element]
|
||||
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
||||
|
||||
(defn create-text-span
|
||||
(defn create-inline
|
||||
[element]
|
||||
(let [text (get-text-span-text element)]
|
||||
(let [text (get-inline-text element)]
|
||||
(d/merge {:text text
|
||||
:key (.-id element)}
|
||||
(get-text-span-styles element))))
|
||||
(get-inline-styles element))))
|
||||
|
||||
(defn create-paragraph
|
||||
[element]
|
||||
(d/merge {:type "paragraph"
|
||||
:key (.-id element)
|
||||
:children (mapv create-text-span (.-children element))}
|
||||
:children (mapv create-inline (.-children element))}
|
||||
(get-paragraph-styles element)))
|
||||
|
||||
(defn create-root
|
||||
|
||||
@@ -92,7 +92,7 @@
|
||||
[root]
|
||||
(get-styles-from-attrs root txt/root-attrs txt/default-text-attrs))
|
||||
|
||||
(defn get-text-span-styles
|
||||
(defn get-inline-styles
|
||||
[inline paragraph]
|
||||
(let [node (if (= "" (:text inline)) paragraph inline)
|
||||
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
||||
@@ -104,7 +104,7 @@
|
||||
(when text
|
||||
(.replace text (js/RegExp "/" "g") "/\u200B")))
|
||||
|
||||
(defn get-text-span-children
|
||||
(defn get-inline-children
|
||||
[inline paragraph]
|
||||
[(if (and (= "" (:text inline))
|
||||
(= 1 (count (:children paragraph))))
|
||||
@@ -119,14 +119,14 @@
|
||||
[paragraph]
|
||||
(some #(not= "" (:text % "")) (:children paragraph)))
|
||||
|
||||
(defn create-text-span
|
||||
(defn create-inline
|
||||
[inline paragraph]
|
||||
(create-element
|
||||
"span"
|
||||
{:id (or (:key inline) (create-random-key))
|
||||
:data {:itype "span"}
|
||||
:style (get-text-span-styles inline paragraph)}
|
||||
(get-text-span-children inline paragraph)))
|
||||
:data {:itype "inline"}
|
||||
:style (get-inline-styles inline paragraph)}
|
||||
(get-inline-children inline paragraph)))
|
||||
|
||||
(defn create-paragraph
|
||||
[paragraph]
|
||||
@@ -135,7 +135,7 @@
|
||||
{:id (or (:key paragraph) (create-random-key))
|
||||
:data {:itype "paragraph"}
|
||||
:style (get-paragraph-styles paragraph)}
|
||||
(mapv #(create-text-span % paragraph) (:children paragraph))))
|
||||
(mapv #(create-inline % paragraph) (:children paragraph))))
|
||||
|
||||
(defn create-root
|
||||
[root]
|
||||
|
||||
@@ -58,8 +58,6 @@
|
||||
|
||||
(swap! state update ::snap snap/update-page old-page new-page)
|
||||
(swap! state update ::selection selection/update-page old-page new-page))
|
||||
(catch :default cause
|
||||
(log/error :hint "error updating page index" :id page-id :cause cause))
|
||||
(finally
|
||||
(let [elapsed (tpoint)]
|
||||
(log/dbg :hint "page index updated" :id page-id :elapsed elapsed ::log/sync? true))))
|
||||
|
||||
@@ -179,7 +179,6 @@
|
||||
|
||||
(->> (render-canvas-blob canvas width height bgcolor)
|
||||
(p/fnly (fn [data cause]
|
||||
(wasm.api/clear-canvas)
|
||||
(if cause
|
||||
(rx/error! subs cause)
|
||||
(rx/push! subs
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"@vitest/browser": "^1.6.0",
|
||||
"@vitest/coverage-v8": "^1.6.0",
|
||||
"@vitest/ui": "^1.6.0",
|
||||
"canvas": "^3.2.1",
|
||||
"esbuild": "^0.24.0",
|
||||
"jsdom": "^25.0.0",
|
||||
"playwright": "^1.45.1",
|
||||
|
||||
@@ -130,9 +130,9 @@ export class TextEditor extends EventTarget {
|
||||
cut: this.#onCut,
|
||||
copy: this.#onCopy,
|
||||
|
||||
keydown: this.#onKeyDown,
|
||||
beforeinput: this.#onBeforeInput,
|
||||
input: this.#onInput,
|
||||
keydown: this.#onKeyDown,
|
||||
};
|
||||
this.#styleDefaults = options?.styleDefaults;
|
||||
this.#options = options;
|
||||
@@ -160,7 +160,7 @@ export class TextEditor extends EventTarget {
|
||||
if (this.#element.ariaAutoComplete) this.#element.ariaAutoComplete = false;
|
||||
if (!this.#element.ariaMultiLine) this.#element.ariaMultiLine = true;
|
||||
this.#element.dataset.itype = "editor";
|
||||
if (options?.shouldUpdatePositionOnScroll) {
|
||||
if (options.shouldUpdatePositionOnScroll) {
|
||||
this.#updatePositionFromCanvas();
|
||||
}
|
||||
}
|
||||
@@ -186,7 +186,7 @@ export class TextEditor extends EventTarget {
|
||||
"stylechange",
|
||||
this.#onStyleChange,
|
||||
);
|
||||
if (options?.shouldUpdatePositionOnScroll) {
|
||||
if (options.shouldUpdatePositionOnScroll) {
|
||||
window.addEventListener("scroll", this.#onScroll);
|
||||
}
|
||||
addEventListeners(this.#element, this.#events, {
|
||||
@@ -218,7 +218,7 @@ export class TextEditor extends EventTarget {
|
||||
|
||||
// Disposes the rest of event listeners.
|
||||
removeEventListeners(this.#element, this.#events);
|
||||
if (this.#options?.shouldUpdatePositionOnScroll) {
|
||||
if (this.#options.shouldUpdatePositionOnScroll) {
|
||||
window.removeEventListener("scroll", this.#onScroll);
|
||||
}
|
||||
|
||||
@@ -385,8 +385,7 @@ export class TextEditor extends EventTarget {
|
||||
* @param {InputEvent} e
|
||||
*/
|
||||
#onBeforeInput = (e) => {
|
||||
if (e.inputType === "historyUndo"
|
||||
|| e.inputType === "historyRedo") {
|
||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -420,8 +419,7 @@ export class TextEditor extends EventTarget {
|
||||
* @param {InputEvent} e
|
||||
*/
|
||||
#onInput = (e) => {
|
||||
if (e.inputType === "historyUndo"
|
||||
|| e.inputType === "historyRedo") {
|
||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { getFills } from "./Color.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Color", () => {
|
||||
test("getFills", () => {
|
||||
expect(getFills("#aa0000")).toBe(
|
||||
'[["^ ","~:fill-color","#aa0000","~:fill-opacity",1]]',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -31,9 +31,9 @@ describe("Content", () => {
|
||||
inertElement.style,
|
||||
);
|
||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||
expect(contentFragment.children).toHaveLength(2);
|
||||
expect(contentFragment.children).toHaveLength(1);
|
||||
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
|
||||
expect(contentFragment.firstElementChild.children).toHaveLength(1);
|
||||
expect(contentFragment.firstElementChild.children).toHaveLength(2);
|
||||
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
|
||||
HTMLSpanElement,
|
||||
);
|
||||
@@ -43,7 +43,6 @@ describe("Content", () => {
|
||||
expect(contentFragment.textContent).toBe("Hello, World!");
|
||||
});
|
||||
|
||||
/*
|
||||
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple paragraphs)", () => {
|
||||
const paragraphs = [
|
||||
"Lorem ipsum",
|
||||
@@ -52,11 +51,11 @@ describe("Content", () => {
|
||||
];
|
||||
const inertElement = document.createElement("div");
|
||||
const contentFragment = mapContentFragmentFromHTML(
|
||||
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
||||
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div><br/></div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
||||
inertElement.style,
|
||||
);
|
||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||
expect(contentFragment.children).toHaveLength(5);
|
||||
expect(contentFragment.children).toHaveLength(3);
|
||||
for (let index = 0; index < contentFragment.children.length; index++) {
|
||||
expect(contentFragment.children.item(index)).toBeInstanceOf(
|
||||
HTMLDivElement,
|
||||
@@ -75,7 +74,6 @@ describe("Content", () => {
|
||||
"Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis.",
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
test("mapContentFragmentFromString should return a valid content for the editor", () => {
|
||||
const contentFragment = mapContentFragmentFromString("Hello, \nWorld!");
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import {
|
||||
isEditor,
|
||||
TYPE,
|
||||
TAG,
|
||||
} from "./Editor.js";
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Editor", () => {
|
||||
test("isEditor should return true", () => {
|
||||
const element = document.createElement(TAG)
|
||||
element.dataset.itype = TYPE;
|
||||
expect(isEditor(element)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when element is null", () => {
|
||||
expect(isEditor(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when the tag is not valid", () => {
|
||||
const element = document.createElement("span");
|
||||
expect(isEditor(element)).toBeFalsy();
|
||||
});
|
||||
|
||||
test("isEditor should return false when the itype is not valid", () => {
|
||||
const element = document.createElement(TAG);
|
||||
element.dataset.itype = "whatever";
|
||||
expect(isEditor(element)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
@@ -49,8 +49,7 @@ describe("Element", () => {
|
||||
},
|
||||
allowedStyles: [["text-decoration"]],
|
||||
});
|
||||
// FIXME:
|
||||
// expect(element.style.getPropertyValue("text-decoration")).toBe("underline");
|
||||
expect(element.style.textDecoration).toBe("underline");
|
||||
});
|
||||
|
||||
test("createElement should create a new element with a child", () => {
|
||||
|
||||
@@ -129,36 +129,8 @@ export function createParagraph(textSpans, styles, attrs) {
|
||||
* @param {Object.<string, *>} styles
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
export function createEmptyParagraph(styles, attrs) {
|
||||
return createParagraph([createEmptyTextSpan(styles)], styles, attrs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new paragraph with text.
|
||||
*
|
||||
* @param {Array<string>|string} text
|
||||
* @param {Object.<string, *>|CSSStyleDeclaration} styles
|
||||
* @param {Object.<string, *>} attrs
|
||||
* @returns {HTMLDivElement}
|
||||
*/
|
||||
export function createParagraphWith(text, styles, attrs) {
|
||||
if (typeof text === "string") {
|
||||
if (text === "" || text === "\n") {
|
||||
return createEmptyParagraph(styles, attrs);
|
||||
}
|
||||
return createParagraph([
|
||||
createTextSpan(new Text(text))
|
||||
], styles, attrs);
|
||||
} else if (Array.isArray(text)) {
|
||||
return createParagraph(
|
||||
text.map((text) => {
|
||||
if (text === "" || text === "\n") return createEmptyTextSpan(styles);
|
||||
return createTextSpan(new Text(text), styles);
|
||||
})
|
||||
, styles, attrs);
|
||||
} else {
|
||||
throw new TypeError("Invalid text, it should be an array of strings or a string");
|
||||
}
|
||||
export function createEmptyParagraph(styles) {
|
||||
return createParagraph([createEmptyTextSpan(styles)], styles);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -12,11 +12,8 @@ import {
|
||||
splitParagraph,
|
||||
splitParagraphAtNode,
|
||||
isEmptyParagraph,
|
||||
createParagraphWith,
|
||||
} from "./Paragraph.js";
|
||||
import { createTextSpan, isTextSpan } from "./TextSpan.js";
|
||||
import { isLineBreak } from './LineBreak.js';
|
||||
import { isTextNode } from './TextNode.js';
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
describe("Paragraph", () => {
|
||||
@@ -31,116 +28,36 @@ describe("Paragraph", () => {
|
||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBe(true);
|
||||
});
|
||||
|
||||
test("createParagraphWith should create a new paragraph with text", () => {
|
||||
// "" as empty paragraph.
|
||||
{
|
||||
const emptyParagraph = createParagraphWith("");
|
||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||
}
|
||||
// "\n" as empty paragraph.
|
||||
{
|
||||
const emptyParagraph = createParagraphWith("\n");
|
||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||
}
|
||||
// [""] as empty paragraph.
|
||||
{
|
||||
const emptyParagraph = createParagraphWith([""]);
|
||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||
}
|
||||
// ["\n"] as empty paragraph.
|
||||
{
|
||||
const emptyParagraph = createParagraphWith(["\n"]);
|
||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||
}
|
||||
// "Lorem ipsum" as a paragraph with a text span.
|
||||
{
|
||||
const paragraph = createParagraphWith("Lorem ipsum");
|
||||
expect(paragraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(paragraph.nodeName).toBe(TAG);
|
||||
expect(paragraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(paragraph.firstChild)).toBeTruthy();
|
||||
expect(isTextNode(paragraph.firstChild.firstChild)).toBeTruthy();
|
||||
expect(paragraph.firstChild.firstChild.textContent).toBe("Lorem ipsum");
|
||||
}
|
||||
// ["Lorem ipsum"] as a paragraph with a text span.
|
||||
{
|
||||
const paragraph = createParagraphWith(["Lorem ipsum"]);
|
||||
expect(paragraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(paragraph.nodeName).toBe(TAG);
|
||||
expect(paragraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(paragraph.firstChild)).toBeTruthy();
|
||||
expect(isTextNode(paragraph.firstChild.firstChild)).toBeTruthy();
|
||||
expect(paragraph.firstChild.firstChild.textContent).toBe("Lorem ipsum");
|
||||
}
|
||||
// ["Lorem ipsum","\n","dolor sit amet"] as a paragraph with multiple text spans.
|
||||
{
|
||||
const paragraph = createParagraphWith(["Lorem ipsum", "\n", "dolor sit amet"]);
|
||||
expect(paragraph).toBeInstanceOf(HTMLDivElement);
|
||||
expect(paragraph.nodeName).toBe(TAG);
|
||||
expect(paragraph.dataset.itype).toBe(TYPE);
|
||||
expect(isTextSpan(paragraph.children.item(0))).toBeTruthy();
|
||||
expect(isTextNode(paragraph.children.item(0).firstChild)).toBeTruthy();
|
||||
expect(paragraph.children.item(0).firstChild.textContent).toBe("Lorem ipsum");
|
||||
expect(isTextSpan(paragraph.children.item(1))).toBeTruthy();
|
||||
expect(isLineBreak(paragraph.children.item(1).firstChild)).toBeTruthy();
|
||||
expect(isTextSpan(paragraph.children.item(2))).toBeTruthy();
|
||||
expect(isTextNode(paragraph.children.item(2).firstChild)).toBeTruthy();
|
||||
expect(paragraph.children.item(2).firstChild.textContent).toBe("dolor sit amet");
|
||||
}
|
||||
{
|
||||
expect(() => {
|
||||
createParagraphWith({});
|
||||
}).toThrow("Invalid text, it should be an array of strings or a string");
|
||||
}
|
||||
})
|
||||
|
||||
test("isParagraph should return true when the passed node is a paragraph", () => {
|
||||
expect(isParagraph(null)).toBeFalsy();
|
||||
expect(isParagraph(document.createElement("div"))).toBeFalsy();
|
||||
expect(isParagraph(document.createElement("h1"))).toBeFalsy();
|
||||
expect(isParagraph(createEmptyParagraph())).toBeTruthy();
|
||||
expect(isParagraph(null)).toBe(false);
|
||||
expect(isParagraph(document.createElement("div"))).toBe(false);
|
||||
expect(isParagraph(document.createElement("h1"))).toBe(false);
|
||||
expect(isParagraph(createEmptyParagraph())).toBe(true);
|
||||
expect(
|
||||
isParagraph(createParagraph([createTextSpan(new Text("Hello, World!"))])),
|
||||
).toBeTruthy();
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("isLikeParagraph should return true when node looks like a paragraph", () => {
|
||||
const p = document.createElement("p");
|
||||
expect(isLikeParagraph(p)).toBeTruthy();
|
||||
expect(isLikeParagraph(p)).toBe(true);
|
||||
const div = document.createElement("div");
|
||||
expect(isLikeParagraph(div)).toBeTruthy();
|
||||
expect(isLikeParagraph(div)).toBe(true);
|
||||
const h1 = document.createElement("h1");
|
||||
expect(isLikeParagraph(h1)).toBeTruthy();
|
||||
expect(isLikeParagraph(h1)).toBe(true);
|
||||
const h2 = document.createElement("h2");
|
||||
expect(isLikeParagraph(h2)).toBeTruthy();
|
||||
expect(isLikeParagraph(h2)).toBe(true);
|
||||
const h3 = document.createElement("h3");
|
||||
expect(isLikeParagraph(h3)).toBeTruthy();
|
||||
expect(isLikeParagraph(h3)).toBe(true);
|
||||
const h4 = document.createElement("h4");
|
||||
expect(isLikeParagraph(h4)).toBeTruthy();
|
||||
expect(isLikeParagraph(h4)).toBe(true);
|
||||
const h5 = document.createElement("h5");
|
||||
expect(isLikeParagraph(h5)).toBeTruthy();
|
||||
expect(isLikeParagraph(h5)).toBe(true);
|
||||
const h6 = document.createElement("h6");
|
||||
expect(isLikeParagraph(h6)).toBeTruthy();
|
||||
expect(isLikeParagraph(h6)).toBe(true);
|
||||
});
|
||||
|
||||
test("getParagraph should return the closest paragraph of the passed node", () => {
|
||||
@@ -159,34 +76,26 @@ describe("Paragraph", () => {
|
||||
|
||||
test("isParagraphStart should return true on an empty paragraph", () => {
|
||||
const paragraph = createEmptyParagraph();
|
||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBeTruthy();
|
||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("isParagraphStart should return true on a paragraph", () => {
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text("Hello, World!")),
|
||||
]);
|
||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBeTruthy();
|
||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("isParagraphEnd should return true on an empty paragraph", () => {
|
||||
const paragraph = createEmptyParagraph();
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 0)).toBeTruthy();
|
||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 0)).toBe(true);
|
||||
});
|
||||
|
||||
test("isParagraphEnd should return true on a paragraph", () => {
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text("Hello, World!")),
|
||||
]);
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 13)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("isParagraphEnd should return false on a paragrah where the focus offset is inside", () => {
|
||||
const paragraph = createParagraph([
|
||||
createTextSpan(new Text("Lorem ipsum sit")),
|
||||
createTextSpan(new Text("amet")),
|
||||
]);
|
||||
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 15)).toBeFalsy();
|
||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
|
||||
});
|
||||
|
||||
test("splitParagraph should split a paragraph", () => {
|
||||
@@ -225,14 +134,14 @@ describe("Paragraph", () => {
|
||||
const div = document.createElement("div");
|
||||
const blockquote = document.createElement("blockquote");
|
||||
const table = document.createElement("table");
|
||||
expect(isLikeParagraph(span)).toBeFalsy();
|
||||
expect(isLikeParagraph(a)).toBeFalsy();
|
||||
expect(isLikeParagraph(br)).toBeFalsy();
|
||||
expect(isLikeParagraph(i)).toBeFalsy();
|
||||
expect(isLikeParagraph(u)).toBeFalsy();
|
||||
expect(isLikeParagraph(div)).toBeTruthy();
|
||||
expect(isLikeParagraph(blockquote)).toBeTruthy();
|
||||
expect(isLikeParagraph(table)).toBeTruthy();
|
||||
expect(isLikeParagraph(span)).toBe(false);
|
||||
expect(isLikeParagraph(a)).toBe(false);
|
||||
expect(isLikeParagraph(br)).toBe(false);
|
||||
expect(isLikeParagraph(i)).toBe(false);
|
||||
expect(isLikeParagraph(u)).toBe(false);
|
||||
expect(isLikeParagraph(div)).toBe(true);
|
||||
expect(isLikeParagraph(blockquote)).toBe(true);
|
||||
expect(isLikeParagraph(table)).toBe(true);
|
||||
});
|
||||
|
||||
test("isEmptyParagraph should return true if the paragraph is empty", () => {
|
||||
@@ -253,7 +162,7 @@ describe("Paragraph", () => {
|
||||
const emptyParagraph = document.createElement("div");
|
||||
emptyParagraph.dataset.itype = "paragraph";
|
||||
emptyParagraph.appendChild(emptyTextSpan);
|
||||
expect(isEmptyParagraph(emptyParagraph)).toBeTruthy();
|
||||
expect(isEmptyParagraph(emptyParagraph)).toBe(true);
|
||||
|
||||
const nonEmptyTextSpan = document.createElement("span");
|
||||
nonEmptyTextSpan.dataset.itype = "span";
|
||||
@@ -261,6 +170,6 @@ describe("Paragraph", () => {
|
||||
const nonEmptyParagraph = document.createElement("div");
|
||||
nonEmptyParagraph.dataset.itype = "paragraph";
|
||||
nonEmptyParagraph.appendChild(nonEmptyTextSpan);
|
||||
expect(isEmptyParagraph(nonEmptyParagraph)).toBeFalsy();
|
||||
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,11 +30,10 @@ describe("Root", () => {
|
||||
test("setRootStyles should apply only the styles of root to the root", () => {
|
||||
const emptyRoot = createEmptyRoot();
|
||||
setRootStyles(emptyRoot, {
|
||||
"--vertical-align": "top",
|
||||
"font-size": "25px",
|
||||
["--vertical-align"]: "top",
|
||||
["font-size"]: "25px",
|
||||
});
|
||||
// FIXME:
|
||||
// expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
||||
expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
||||
// We expect this style to be empty because we don't apply it
|
||||
// to the root.
|
||||
expect(emptyRoot.style.getPropertyValue("font-size")).toBe("");
|
||||
|
||||
@@ -243,9 +243,6 @@ export function normalizeStyles(
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function setStyle(element, styleName, styleValue, styleUnit) {
|
||||
if (styleValue === "mixed")
|
||||
return element;
|
||||
|
||||
if (
|
||||
styleName.startsWith("--") &&
|
||||
typeof styleValue !== "string" &&
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Style", () => {
|
||||
"font-size": "32px",
|
||||
display: "none",
|
||||
});
|
||||
expect(element.style.display).toBe("");
|
||||
expect(element.style.display).toBe("none");
|
||||
expect(element.style.fontSize).toBe("");
|
||||
expect(element.style.textDecoration).toBe("");
|
||||
});
|
||||
@@ -32,13 +32,13 @@ describe("Style", () => {
|
||||
setStyles(a, [["display"]], {
|
||||
display: "none",
|
||||
});
|
||||
expect(a.style.display).toBe("");
|
||||
expect(a.style.display).toBe("none");
|
||||
expect(a.style.fontSize).toBe("");
|
||||
expect(a.style.textDecoration).toBe("");
|
||||
|
||||
const b = document.createElement("div");
|
||||
setStyles(b, [["display"]], a.style);
|
||||
expect(b.style.display).toBe("");
|
||||
expect(b.style.display).toBe("none");
|
||||
expect(b.style.fontSize).toBe("");
|
||||
expect(b.style.textDecoration).toBe("");
|
||||
});
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
* Copyright (c) KALEIDOS INC
|
||||
*/
|
||||
|
||||
import { SafeGuard } from "../../controllers/SafeGuard.js";
|
||||
import SafeGuard from "../../controllers/SafeGuard.js";
|
||||
|
||||
/**
|
||||
* Iterator direction.
|
||||
@@ -29,7 +29,6 @@ export class TextNodeIterator {
|
||||
* @returns {boolean}
|
||||
*/
|
||||
static isTextNode(node) {
|
||||
if (node === null) debugger;
|
||||
return (
|
||||
node.nodeType === Node.TEXT_NODE ||
|
||||
(node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR")
|
||||
@@ -274,11 +273,10 @@ export class TextNodeIterator {
|
||||
*iterateFrom(startNode, endNode) {
|
||||
const comparedPosition = startNode.compareDocumentPosition(endNode);
|
||||
this.#currentNode = startNode;
|
||||
const safeGuard = new SafeGuard("TextNodeIterator");
|
||||
safeGuard.start();
|
||||
SafeGuard.start();
|
||||
while (this.#currentNode !== endNode) {
|
||||
yield this.#currentNode;
|
||||
safeGuard.update();
|
||||
SafeGuard.update();
|
||||
if (comparedPosition === Node.DOCUMENT_POSITION_PRECEDING) {
|
||||
if (!this.previousNode()) {
|
||||
break;
|
||||
|
||||
@@ -17,7 +17,7 @@ import { setStyles, mergeStyles } from "./Style.js";
|
||||
import { createRandomId } from "./Element.js";
|
||||
|
||||
export const TAG = "SPAN";
|
||||
export const TYPE = "span";
|
||||
export const TYPE = "inline";
|
||||
export const QUERY = `[data-itype="${TYPE}"]`;
|
||||
export const STYLES = [
|
||||
["--typography-ref-id"],
|
||||
|
||||
@@ -18,7 +18,7 @@ import { createLineBreak } from "./LineBreak.js";
|
||||
describe("TextSpan", () => {
|
||||
test("createTextSpan should throw when passed an invalid child", () => {
|
||||
expect(() => createTextSpan("Hello, World!")).toThrowError(
|
||||
"Invalid text span child",
|
||||
"Invalid textSpan child",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ describe("TextSpan", () => {
|
||||
|
||||
test("getTextSpanLength throws when the passed node is not an textSpan", () => {
|
||||
const textSpan = document.createElement("div");
|
||||
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid text span");
|
||||
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid textSpan");
|
||||
});
|
||||
|
||||
test("getTextSpanLength returns the length of the textSpan content", () => {
|
||||
|
||||
@@ -1,85 +1,47 @@
|
||||
/**
|
||||
* Safe guard.
|
||||
* Max. amount of time we should allow.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
export class SafeGuard {
|
||||
/**
|
||||
* Maximum time.
|
||||
*
|
||||
* @readonly
|
||||
* @type {number}
|
||||
*/
|
||||
static MAX_TIME = 1000
|
||||
const SAFE_GUARD_TIME = 1000;
|
||||
|
||||
/**
|
||||
* Maximum time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
#maxTime = SafeGuard.MAX_TIME
|
||||
/**
|
||||
* Time at which the safeguard started.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
let startTime = Date.now();
|
||||
|
||||
/**
|
||||
* Start time.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
#startTime = 0
|
||||
/**
|
||||
* Marks the start of the safeguard.
|
||||
*/
|
||||
export function start() {
|
||||
startTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Context
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
#context = ""
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {string} [context]
|
||||
* @param {number} [maxTime=SafeGuard.MAX_TIME]
|
||||
* @param {number} [startTime=Date.now()]
|
||||
*/
|
||||
constructor(context, maxTime = SafeGuard.MAX_TIME, startTime = Date.now()) {
|
||||
this.#context = context
|
||||
this.#maxTime = maxTime;
|
||||
this.#startTime = startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Safe guard context.
|
||||
*
|
||||
* @type {string}
|
||||
*/
|
||||
get context() {
|
||||
return this.#context
|
||||
}
|
||||
|
||||
/**
|
||||
* Time elapsed.
|
||||
*
|
||||
* @type {number}
|
||||
*/
|
||||
get elapsed() {
|
||||
return Date.now() - this.#startTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts the safe guard timer.
|
||||
*/
|
||||
start() {
|
||||
this.#startTime = Date.now();
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the safe guard timer.
|
||||
*
|
||||
* @throws
|
||||
*/
|
||||
update() {
|
||||
if (this.elapsed >= this.#maxTime) {
|
||||
throw new Error(`Safe guard timeout "${this.#context}"`);
|
||||
}
|
||||
/**
|
||||
* Checks if the safeguard should throw.
|
||||
*/
|
||||
export function update() {
|
||||
if (Date.now - startTime >= SAFE_GUARD_TIME) {
|
||||
throw new Error("Safe guard timeout");
|
||||
}
|
||||
}
|
||||
|
||||
export default SafeGuard;
|
||||
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,
|
||||
};
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import { describe, test, expect } from "vitest";
|
||||
import { SafeGuard } from "./SafeGuard.js";
|
||||
|
||||
describe("SafeGuard", () => {
|
||||
test("create a new SafeGuard", () => {
|
||||
const safeGuard = new SafeGuard("Context");
|
||||
expect(safeGuard.context).toBe("Context");
|
||||
expect(safeGuard.elapsed).toBeLessThan(100);
|
||||
});
|
||||
|
||||
test("SafeGuard throws an error when too much time is spent", () => {
|
||||
expect(() => {
|
||||
const safeGuard = new SafeGuard("Context", 100);
|
||||
safeGuard.start();
|
||||
// NOTE: This is the type of loop we try to
|
||||
// be safe.
|
||||
while (true) {
|
||||
safeGuard.update();
|
||||
}
|
||||
}).toThrow('Safe guard timeout "Context"');
|
||||
});
|
||||
});
|
||||
@@ -52,7 +52,7 @@ import TextEditor from "../TextEditor.js";
|
||||
import CommandMutations from "../commands/CommandMutations.js";
|
||||
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
||||
import { SelectionDirection } from "./SelectionDirection.js";
|
||||
import { SafeGuard } from "./SafeGuard.js";
|
||||
import SafeGuard from "./SafeGuard.js";
|
||||
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
||||
import StyleDeclaration from "./StyleDeclaration.js";
|
||||
|
||||
@@ -167,7 +167,7 @@ export class SelectionController extends EventTarget {
|
||||
/**
|
||||
* @type {TextEditorOptions}
|
||||
*/
|
||||
#options = {};
|
||||
#options;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
@@ -185,7 +185,7 @@ export class SelectionController extends EventTarget {
|
||||
throw new TypeError("Invalid EventTarget");
|
||||
}
|
||||
*/
|
||||
this.#options = options ?? {};
|
||||
this.#options = options;
|
||||
this.#debug = options?.debug;
|
||||
this.#styleDefaults = options?.styleDefaults;
|
||||
this.#selection = selection;
|
||||
@@ -238,11 +238,11 @@ export class SelectionController extends EventTarget {
|
||||
#applyStylesFromElementToCurrentStyle(element) {
|
||||
for (let index = 0; index < element.style.length; index++) {
|
||||
const styleName = element.style.item(index);
|
||||
// Only merge fill styles from text spans.
|
||||
if (!isTextSpan(element) && styleName === "--fills") {
|
||||
if (styleName === "--fills") {
|
||||
continue;
|
||||
}
|
||||
let styleValue = element.style.getPropertyValue(styleName);
|
||||
|
||||
if (styleName === "font-family") {
|
||||
styleValue = sanitizeFontFamily(styleValue);
|
||||
}
|
||||
@@ -277,29 +277,22 @@ export class SelectionController extends EventTarget {
|
||||
this.#applyDefaultStylesToCurrentStyle();
|
||||
const root = startNode.parentElement.parentElement.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(root);
|
||||
if (startNode === endNode) {
|
||||
const paragraph = startNode.parentElement.parentElement;
|
||||
// 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);
|
||||
const textSpan = startNode.parentElement;
|
||||
this.#applyStylesFromElementToCurrentStyle(textSpan);
|
||||
} else {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
for (const textNode of this.#textNodeIterator.iterateFrom(
|
||||
startNode,
|
||||
endNode,
|
||||
)) {
|
||||
const textSpan = textNode.parentElement;
|
||||
this.#mergeStylesFromElementToCurrentStyle(textSpan);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
@@ -1699,8 +1692,7 @@ export class SelectionController extends EventTarget {
|
||||
* @param {RemoveSelectedOptions} [options]
|
||||
*/
|
||||
removeSelected(options) {
|
||||
if (this.isCollapsed)
|
||||
return;
|
||||
if (this.isCollapsed) return;
|
||||
|
||||
const affectedTextSpans = new Set();
|
||||
const affectedParagraphs = new Set();
|
||||
@@ -1709,6 +1701,7 @@ export class SelectionController extends EventTarget {
|
||||
let nextNode = null;
|
||||
|
||||
let { startNode, endNode, startOffset, endOffset } = this.getRanges();
|
||||
|
||||
if (this.shouldHandleCompleteDeletion(startNode, endNode)) {
|
||||
return this.handleCompleteContentDeletion();
|
||||
}
|
||||
@@ -1753,10 +1746,9 @@ export class SelectionController extends EventTarget {
|
||||
const endTextSpan = getTextSpan(endNode);
|
||||
const endParagraph = getParagraph(endNode);
|
||||
|
||||
const safeGuard = new SafeGuard("removeSelected");
|
||||
safeGuard.start();
|
||||
SafeGuard.start();
|
||||
do {
|
||||
safeGuard.update();
|
||||
SafeGuard.update();
|
||||
|
||||
const { currentNode } = this.#textNodeIterator;
|
||||
|
||||
@@ -1768,8 +1760,6 @@ export class SelectionController extends EventTarget {
|
||||
affectedParagraphs.add(paragraph);
|
||||
|
||||
let shouldRemoveNodeCompletely = false;
|
||||
const isEndNode = currentNode === endNode;
|
||||
|
||||
if (currentNode === startNode) {
|
||||
if (startOffset === 0) {
|
||||
// We should remove this node completely.
|
||||
@@ -1778,11 +1768,11 @@ export class SelectionController extends EventTarget {
|
||||
// We should remove this node partially.
|
||||
currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
|
||||
}
|
||||
} else if (isEndNode) {
|
||||
} else if (currentNode === endNode) {
|
||||
if (
|
||||
isLineBreak(endNode) ||
|
||||
(isTextNode(endNode) &&
|
||||
endOffset >= (endNode.nodeValue?.length || 0))
|
||||
endOffset === (endNode.nodeValue?.length || 0))
|
||||
) {
|
||||
// We should remove this node completely.
|
||||
shouldRemoveNodeCompletely = true;
|
||||
@@ -1795,13 +1785,9 @@ export class SelectionController extends EventTarget {
|
||||
shouldRemoveNodeCompletely = true;
|
||||
}
|
||||
|
||||
// We need to step to the next node before
|
||||
// we remove them completely from the DOM tree
|
||||
// because we need to iterate through parents
|
||||
// and childrens.
|
||||
this.#textNodeIterator.nextNode();
|
||||
|
||||
// We remove the current node.
|
||||
// Realizamos el borrado del nodo actual.
|
||||
if (shouldRemoveNodeCompletely) {
|
||||
currentNode.remove();
|
||||
if (currentNode === startNode) {
|
||||
@@ -1812,14 +1798,12 @@ export class SelectionController extends EventTarget {
|
||||
textSpan.remove();
|
||||
}
|
||||
|
||||
if (paragraph !== startParagraph
|
||||
&& paragraph.children.length === 0) {
|
||||
if (paragraph !== startParagraph && paragraph.children.length === 0) {
|
||||
paragraph.remove();
|
||||
}
|
||||
}
|
||||
|
||||
// Break immediately after processing endNode, before advancing iterator
|
||||
if (isEndNode) {
|
||||
if (currentNode === endNode) {
|
||||
break;
|
||||
}
|
||||
} while (this.#textNodeIterator.currentNode);
|
||||
@@ -1870,28 +1854,16 @@ export class SelectionController extends EventTarget {
|
||||
return this.collapse(startNode, startOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object with ranges.
|
||||
*
|
||||
* @returns {}
|
||||
*/
|
||||
getRanges() {
|
||||
let startNode = getClosestTextNode(this.#range.startContainer);
|
||||
let endNode = getClosestTextNode(this.#range.endContainer);
|
||||
|
||||
let startOffset = this.#range.startOffset;
|
||||
let endOffset = this.#range.endOffset;
|
||||
let endOffset = this.#range.startOffset + this.#range.toString().length;
|
||||
|
||||
return { startNode, endNode, startOffset, endOffset };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if we should remove the complete root.
|
||||
*
|
||||
* @param {*} startNode
|
||||
* @param {*} endNode
|
||||
* @returns {boolean}
|
||||
*/
|
||||
shouldHandleCompleteDeletion(startNode, endNode) {
|
||||
const root = this.#textEditor.root;
|
||||
return (
|
||||
@@ -2019,12 +1991,11 @@ export class SelectionController extends EventTarget {
|
||||
// then we need to iterate through those nodes to apply
|
||||
// the styles.
|
||||
} else if (startNode !== endNode) {
|
||||
const safeGuard = new SafeGuard("applyStylesTo");
|
||||
safeGuard.start();
|
||||
SafeGuard.start();
|
||||
const expectedEndNode = getClosestTextNode(endNode);
|
||||
this.#textNodeIterator.currentNode = getClosestTextNode(startNode);
|
||||
do {
|
||||
safeGuard.update();
|
||||
SafeGuard.update();
|
||||
|
||||
const paragraph = getParagraph(this.#textNodeIterator.currentNode);
|
||||
setParagraphStyles(paragraph, newStyles);
|
||||
|
||||
@@ -2,14 +2,12 @@ import { expect, describe, test } from "vitest";
|
||||
import {
|
||||
createEmptyParagraph,
|
||||
createParagraph,
|
||||
createParagraphWith,
|
||||
} from "../content/dom/Paragraph.js";
|
||||
import { createTextSpan } from "../content/dom/TextSpan.js";
|
||||
import { createLineBreak } from "../content/dom/LineBreak.js";
|
||||
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
||||
import { SelectionController } from "./SelectionController.js";
|
||||
import { SelectionDirection } from "./SelectionDirection.js";
|
||||
import StyleDeclaration from './StyleDeclaration.js';
|
||||
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
@@ -37,26 +35,6 @@ function focus(
|
||||
}
|
||||
|
||||
describe("SelectionController", () => {
|
||||
test("`options` should return the Options object kept by the SelectionController", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
expect(selectionController.options).toStrictEqual({});
|
||||
});
|
||||
|
||||
test("`currentStyle` should return the StyleDeclaration object kept by the SelectionController", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
expect(selectionController.currentStyle).toBeInstanceOf(StyleDeclaration);
|
||||
});
|
||||
|
||||
test("`selection` should return the Selection object kept by the SelectionController", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||
const selection = document.getSelection();
|
||||
@@ -268,7 +246,7 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a text span from a pasted fragment (at start)", () => {
|
||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at start)", () => {
|
||||
const textEditorMock =
|
||||
TextEditorMock.createTextEditorMockWithText(", World!");
|
||||
const root = textEditorMock.root;
|
||||
@@ -278,7 +256,7 @@ describe("SelectionController", () => {
|
||||
selection,
|
||||
);
|
||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||
const paragraph = createParagraphWith(["Hello"]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
|
||||
@@ -300,12 +278,12 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Hello",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
", World!",
|
||||
);
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a text span from a pasted fragment (at middle)", () => {
|
||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at middle)", () => {
|
||||
const textEditorMock =
|
||||
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
||||
const root = textEditorMock.root;
|
||||
@@ -320,12 +298,11 @@ describe("SelectionController", () => {
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Lorem ".length,
|
||||
);
|
||||
const paragraph = createParagraphWith(["ipsum "]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
|
||||
selectionController.insertPaste(fragment);
|
||||
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||
@@ -340,18 +317,18 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||
Text,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.children.item(0).firstChild.nodeValue).toBe(
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Lorem ",
|
||||
);
|
||||
expect(
|
||||
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||
textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue,
|
||||
).toBe("ipsum ");
|
||||
expect(textEditorMock.root.firstChild.children.item(2).firstChild.nodeValue).toBe(
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"dolor",
|
||||
);
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a text span from a pasted fragment (at end)", () => {
|
||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at end)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -365,7 +342,7 @@ describe("SelectionController", () => {
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Hello".length,
|
||||
);
|
||||
const paragraph = createParagraphWith([", World!"]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
|
||||
@@ -387,7 +364,7 @@ describe("SelectionController", () => {
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Hello",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
||||
", World!",
|
||||
);
|
||||
});
|
||||
@@ -402,7 +379,7 @@ describe("SelectionController", () => {
|
||||
selection,
|
||||
);
|
||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||
const paragraph = createParagraphWith(["Hello"]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
@@ -430,7 +407,7 @@ describe("SelectionController", () => {
|
||||
).toBe(", World!");
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a text span from a pasted fragment (at middle)", () => {
|
||||
test("`insertPaste` should insert an text span from a pasted fragment (at middle)", () => {
|
||||
const textEditorMock =
|
||||
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
||||
const root = textEditorMock.root;
|
||||
@@ -445,7 +422,7 @@ describe("SelectionController", () => {
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Lorem ".length,
|
||||
);
|
||||
const paragraph = createParagraphWith(["ipsum "]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
@@ -476,7 +453,7 @@ describe("SelectionController", () => {
|
||||
).toBe("dolor");
|
||||
});
|
||||
|
||||
test("`insertPaste` should insert a text span from a pasted fragment (at end)", () => {
|
||||
test("`insertPaste` should insert an text span from a pasted fragment (at end)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -490,7 +467,7 @@ describe("SelectionController", () => {
|
||||
root.firstChild.firstChild.firstChild,
|
||||
"Hello".length,
|
||||
);
|
||||
const paragraph = createParagraphWith([", World!"]);
|
||||
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
||||
paragraph.dataset.textSpan = "force";
|
||||
const fragment = document.createDocumentFragment();
|
||||
fragment.append(paragraph);
|
||||
@@ -582,9 +559,9 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["World!"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||
createParagraph([createTextSpan(new Text("World!"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -614,10 +591,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["\n"],
|
||||
["World!"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||
createEmptyParagraph(),
|
||||
createParagraph([createTextSpan(new Text("World!"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -649,9 +626,9 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["World!"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||
createParagraph([createTextSpan(new Text("World!"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -681,10 +658,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["\n"],
|
||||
["World!"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||
createEmptyParagraph(),
|
||||
createParagraph([createTextSpan(new Text("World!"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -783,10 +760,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 completelly selected)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||
"Hello, ",
|
||||
"World!",
|
||||
]]);
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||
createTextSpan(new Text("Hello, ")),
|
||||
createTextSpan(new Text("World!")),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
@@ -824,10 +801,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 partially selected)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||
"Hello, ",
|
||||
"World!",
|
||||
]]);
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||
createTextSpan(new Text("Hello, ")),
|
||||
createTextSpan(new Text("World!")),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
@@ -870,10 +847,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 partially selected, 1 completelly selected)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||
"Hello, ",
|
||||
"World!",
|
||||
]]);
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||
createTextSpan(new Text("Hello, ")),
|
||||
createTextSpan(new Text("World!")),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
@@ -909,9 +886,7 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
// FIXME: I don't know why but this test blocks all the tests.
|
||||
/*
|
||||
test.skip("`replaceTextSpans` should replace the selected text in multiple text spans (1 completelly selected, 1 partially selected)", () => {
|
||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 completelly selected, 1 partially selected)", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||
createTextSpan(new Text("Hello, ")),
|
||||
createTextSpan(new Text("World!")),
|
||||
@@ -950,7 +925,6 @@ describe("SelectionController", () => {
|
||||
"Mundold!",
|
||||
);
|
||||
});
|
||||
*/
|
||||
|
||||
test("`removeSelected` removes a word", () => {
|
||||
const textEditorMock =
|
||||
@@ -991,10 +965,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`removeSelected` multiple text spans", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||
"Hello, ",
|
||||
"World!",
|
||||
]]);
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||
createTextSpan(new Text("Hello, ")),
|
||||
createTextSpan(new Text("World!")),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
@@ -1027,11 +1001,11 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test.skip("`removeSelected` multiple paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, "],
|
||||
["\n"],
|
||||
["World!"],
|
||||
test("`removeSelected` multiple paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("World!"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1075,58 +1049,11 @@ describe("SelectionController", () => {
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeSelected` should remove only the selected text from two paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Lorem ipsum"],
|
||||
["dolor sit amet"],
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
const selectionController = new SelectionController(
|
||||
textEditorMock,
|
||||
selection,
|
||||
);
|
||||
focus(
|
||||
selection,
|
||||
textEditorMock,
|
||||
root.firstElementChild.firstElementChild.firstChild,
|
||||
6,
|
||||
root.lastElementChild.firstElementChild.firstChild,
|
||||
9,
|
||||
);
|
||||
selectionController.removeSelected();
|
||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.children).toHaveLength(1);
|
||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||
expect(textEditorMock.root.firstChild.children).toHaveLength(2);
|
||||
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("Lorem amet");
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||
Text,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||
"Lorem ",
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild).toBeInstanceOf(
|
||||
Text,
|
||||
);
|
||||
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||
" amet",
|
||||
);
|
||||
});
|
||||
|
||||
test("`removeSelected` and `removeBackwardParagraph`", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
["\n"],
|
||||
["This is a test"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1166,10 +1093,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`removeSelected` and `removeForwardParagraph`", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
["\n"],
|
||||
["This is a test"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1209,10 +1136,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
["\n"],
|
||||
["This is a test"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1255,10 +1182,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`removeSelected` removes everything", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
["\n"],
|
||||
["This is a test"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1288,10 +1215,10 @@ describe("SelectionController", () => {
|
||||
});
|
||||
|
||||
test("`removeSelected` removes everything and insert text", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||
["Hello, World!"],
|
||||
["\n"],
|
||||
["This is a test"],
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
||||
createParagraph([createTextSpan(createLineBreak())]),
|
||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
@@ -1432,12 +1359,16 @@ describe("SelectionController", () => {
|
||||
|
||||
test("`applyStyles` to paragraphs", () => {
|
||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||
createParagraphWith(["Hello, "], {
|
||||
"font-style": "italic",
|
||||
}),
|
||||
createParagraphWith(["World!"], {
|
||||
"font-style": "oblique",
|
||||
}),
|
||||
createParagraph([
|
||||
createTextSpan(new Text("Hello, "), {
|
||||
"font-style": "italic",
|
||||
}),
|
||||
]),
|
||||
createParagraph([
|
||||
createTextSpan(new Text("World!"), {
|
||||
"font-style": "oblique",
|
||||
}),
|
||||
]),
|
||||
]);
|
||||
const root = textEditorMock.root;
|
||||
const selection = document.getSelection();
|
||||
|
||||
@@ -48,7 +48,7 @@ export class StyleDeclaration {
|
||||
}
|
||||
|
||||
item(index) {
|
||||
return Array.from(this.#items.keys()).at(index);
|
||||
return Array.from(this.#items).at(index).name;
|
||||
}
|
||||
|
||||
removeProperty(name) {
|
||||
|
||||
@@ -29,23 +29,4 @@ describe("StyleDeclaration", () => {
|
||||
expect(styleDeclaration.getPropertyValue("line-height")).toBe("");
|
||||
expect(styleDeclaration.getPropertyPriority("line-height")).toBe("");
|
||||
});
|
||||
|
||||
test("Iterate styles", () => {
|
||||
const properties = [
|
||||
["line-height", "1.2"],
|
||||
["--variable", "hola"],
|
||||
];
|
||||
|
||||
const styleDeclaration = new StyleDeclaration();
|
||||
for (const [name,value] of properties) {
|
||||
styleDeclaration.setProperty(name, value);
|
||||
}
|
||||
for (let index = 0; index < styleDeclaration.length; index++) {
|
||||
const name = styleDeclaration.item(index);
|
||||
const value = styleDeclaration.getPropertyValue(name);
|
||||
const [expectedName, expectedValue] = properties[index];
|
||||
expect(name).toBe(expectedName);
|
||||
expect(value).toBe(expectedValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -462,6 +462,8 @@ class TextEditorPlayground {
|
||||
// Number of text leaves in the paragraph.
|
||||
view.setUint32(0, paragraph.leaves.length, true);
|
||||
|
||||
console.log("lineHeight", paragraph.lineHeight);
|
||||
|
||||
// Serialize paragraph attributes
|
||||
view.setUint8(4, paragraph.textAlign, true); // text-align: left
|
||||
view.setUint8(5, paragraph.textDirection, true); // text-direction: LTR
|
||||
|
||||
@@ -51,6 +51,7 @@ export class TextSpan {
|
||||
elementStyle.getPropertyValue("letter-spacing"),
|
||||
);
|
||||
const fontFamily = elementStyle.getPropertyValue("font-family");
|
||||
console.log("fontFamily", fontFamily);
|
||||
const fontStyles = fontManager.fonts.get(fontFamily);
|
||||
const textDecoration = TextDecoration.fromStyle(
|
||||
elementStyle.getPropertyValue("text-decoration"),
|
||||
@@ -61,6 +62,7 @@ export class TextSpan {
|
||||
const textDirection = TextDirection.fromStyle(
|
||||
elementStyle.getPropertyValue("text-direction"),
|
||||
);
|
||||
console.log(fontWeight, fontStyle);
|
||||
const font = fontStyles.find(
|
||||
(currentFontStyle) =>
|
||||
currentFontStyle.weightAsNumber === fontWeight &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createRoot } from "../editor/content/dom/Root.js";
|
||||
import { createParagraph, createParagraphWith } from "../editor/content/dom/Paragraph.js";
|
||||
import { createParagraph } from "../editor/content/dom/Paragraph.js";
|
||||
import {
|
||||
createEmptyTextSpan,
|
||||
createTextSpan,
|
||||
@@ -67,7 +67,7 @@ export class TextEditorMock extends EventTarget {
|
||||
/**
|
||||
* Creates an empty TextEditor mock.
|
||||
*
|
||||
* @returns {TextEditorMock}
|
||||
* @returns
|
||||
*/
|
||||
static createTextEditorMockEmpty() {
|
||||
const root = createRoot([
|
||||
@@ -83,7 +83,7 @@ export class TextEditorMock extends EventTarget {
|
||||
* created.
|
||||
*
|
||||
* @param {string} text
|
||||
* @returns {TextEditorMock}
|
||||
* @returns
|
||||
*/
|
||||
static createTextEditorMockWithText(text) {
|
||||
return this.createTextEditorMockWithParagraphs([
|
||||
@@ -99,9 +99,8 @@ export class TextEditorMock extends EventTarget {
|
||||
* Creates a TextEditor mock with some textSpans and
|
||||
* only one paragraph.
|
||||
*
|
||||
* @see createTextEditorMockWith
|
||||
* @param {Array<HTMLSpanElement>} textSpans
|
||||
* @returns {TextEditorMock}
|
||||
* @returns
|
||||
*/
|
||||
static createTextEditorMockWithParagraph(textSpans) {
|
||||
return this.createTextEditorMockWithParagraphs([
|
||||
@@ -109,27 +108,10 @@ export class TextEditorMock extends EventTarget {
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a TextEditor mock with some text.
|
||||
*
|
||||
* @param {Array<Array<string>>|Array<string>} paragraphs
|
||||
* @returns {TextEditorMock}
|
||||
*/
|
||||
static createTextEditorMockWith(paragraphs) {
|
||||
const root = createRoot(paragraphs.map((paragraph) => createParagraphWith(paragraph)));
|
||||
return this.createTextEditorMockWithRoot(root);
|
||||
}
|
||||
|
||||
#element = null;
|
||||
#root = null;
|
||||
#selectionImposterElement = null;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param {HTMLDivElement} element
|
||||
* @param {*} options
|
||||
*/
|
||||
constructor(element, options) {
|
||||
super();
|
||||
this.#element = element;
|
||||
|
||||
@@ -515,7 +515,6 @@ __metadata:
|
||||
"@vitest/browser": "npm:^1.6.0"
|
||||
"@vitest/coverage-v8": "npm:^1.6.0"
|
||||
"@vitest/ui": "npm:^1.6.0"
|
||||
canvas: "npm:^3.2.1"
|
||||
esbuild: "npm:^0.24.0"
|
||||
jsdom: "npm:^25.0.0"
|
||||
playwright: "npm:^1.45.1"
|
||||
@@ -903,24 +902,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"base64-js@npm:^1.3.1":
|
||||
version: 1.5.1
|
||||
resolution: "base64-js@npm:1.5.1"
|
||||
checksum: 10c0/f23823513b63173a001030fae4f2dabe283b99a9d324ade3ad3d148e218134676f1ee8568c877cd79ec1c53158dcf2d2ba527a97c606618928ba99dd930102bf
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"bl@npm:^4.0.3":
|
||||
version: 4.1.0
|
||||
resolution: "bl@npm:4.1.0"
|
||||
dependencies:
|
||||
buffer: "npm:^5.5.0"
|
||||
inherits: "npm:^2.0.4"
|
||||
readable-stream: "npm:^3.4.0"
|
||||
checksum: 10c0/02847e1d2cb089c9dc6958add42e3cdeaf07d13f575973963335ac0fdece563a50ac770ac4c8fa06492d2dd276f6cc3b7f08c7cd9c7a7ad0f8d388b2a28def5f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"brace-expansion@npm:^1.1.7":
|
||||
version: 1.1.11
|
||||
resolution: "brace-expansion@npm:1.1.11"
|
||||
@@ -949,16 +930,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"buffer@npm:^5.5.0":
|
||||
version: 5.7.1
|
||||
resolution: "buffer@npm:5.7.1"
|
||||
dependencies:
|
||||
base64-js: "npm:^1.3.1"
|
||||
ieee754: "npm:^1.1.13"
|
||||
checksum: 10c0/27cac81cff434ed2876058d72e7c4789d11ff1120ef32c9de48f59eab58179b66710c488987d295ae89a228f835fc66d088652dffeb8e3ba8659f80eb091d55e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"cac@npm:^6.7.14":
|
||||
version: 6.7.14
|
||||
resolution: "cac@npm:6.7.14"
|
||||
@@ -986,17 +957,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"canvas@npm:^3.2.1":
|
||||
version: 3.2.1
|
||||
resolution: "canvas@npm:3.2.1"
|
||||
dependencies:
|
||||
node-addon-api: "npm:^7.0.0"
|
||||
node-gyp: "npm:latest"
|
||||
prebuild-install: "npm:^7.1.3"
|
||||
checksum: 10c0/c0fd572a8b28e075b40a42b523bdf05e985feaeb18b56085432bfb91a3b905af48f89ec73ed4e795de892cb13f7332ceb0c78cf84c64281c41c29995665b89c8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chai@npm:^4.3.10":
|
||||
version: 4.4.1
|
||||
resolution: "chai@npm:4.4.1"
|
||||
@@ -1021,13 +981,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^1.1.1":
|
||||
version: 1.1.4
|
||||
resolution: "chownr@npm:1.1.4"
|
||||
checksum: 10c0/ed57952a84cc0c802af900cf7136de643d3aba2eecb59d29344bc2f3f9bf703a301b9d84cdc71f82c3ffc9ccde831b0d92f5b45f91727d6c9da62f23aef9d9db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"chownr@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "chownr@npm:2.0.0"
|
||||
@@ -1130,15 +1083,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"decompress-response@npm:^6.0.0":
|
||||
version: 6.0.0
|
||||
resolution: "decompress-response@npm:6.0.0"
|
||||
dependencies:
|
||||
mimic-response: "npm:^3.1.0"
|
||||
checksum: 10c0/bd89d23141b96d80577e70c54fb226b2f40e74a6817652b80a116d7befb8758261ad073a8895648a29cc0a5947021ab66705cb542fa9c143c82022b27c5b175e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-eql@npm:^4.1.3":
|
||||
version: 4.1.4
|
||||
resolution: "deep-eql@npm:4.1.4"
|
||||
@@ -1148,13 +1092,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"deep-extend@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "deep-extend@npm:0.6.0"
|
||||
checksum: 10c0/1c6b0abcdb901e13a44c7d699116d3d4279fdb261983122a3783e7273844d5f2537dc2e1c454a23fcf645917f93fbf8d07101c1d03c015a87faa662755212566
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"delayed-stream@npm:~1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "delayed-stream@npm:1.0.0"
|
||||
@@ -1162,13 +1099,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"detect-libc@npm:^2.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "detect-libc@npm:2.1.2"
|
||||
checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"diff-sequences@npm:^29.6.3":
|
||||
version: 29.6.3
|
||||
resolution: "diff-sequences@npm:29.6.3"
|
||||
@@ -1206,15 +1136,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"end-of-stream@npm:^1.1.0, end-of-stream@npm:^1.4.1":
|
||||
version: 1.4.5
|
||||
resolution: "end-of-stream@npm:1.4.5"
|
||||
dependencies:
|
||||
once: "npm:^1.4.0"
|
||||
checksum: 10c0/b0701c92a10b89afb1cb45bf54a5292c6f008d744eb4382fa559d54775ff31617d1d7bc3ef617575f552e24fad2c7c1a1835948c66b3f3a4be0a6c1f35c883d8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"entities@npm:^4.4.0":
|
||||
version: 4.5.0
|
||||
resolution: "entities@npm:4.5.0"
|
||||
@@ -1425,13 +1346,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"expand-template@npm:^2.0.3":
|
||||
version: 2.0.3
|
||||
resolution: "expand-template@npm:2.0.3"
|
||||
checksum: 10c0/1c9e7afe9acadf9d373301d27f6a47b34e89b3391b1ef38b7471d381812537ef2457e620ae7f819d2642ce9c43b189b3583813ec395e2938319abe356a9b2f51
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"exponential-backoff@npm:^3.1.1":
|
||||
version: 3.1.1
|
||||
resolution: "exponential-backoff@npm:3.1.1"
|
||||
@@ -1505,13 +1419,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-constants@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "fs-constants@npm:1.0.0"
|
||||
checksum: 10c0/a0cde99085f0872f4d244e83e03a46aa387b74f5a5af750896c6b05e9077fac00e9932fdf5aef84f2f16634cd473c63037d7a512576da7d5c2b9163d1909f3a8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"fs-minipass@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "fs-minipass@npm:2.1.0"
|
||||
@@ -1589,13 +1496,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"github-from-package@npm:0.0.0":
|
||||
version: 0.0.0
|
||||
resolution: "github-from-package@npm:0.0.0"
|
||||
checksum: 10c0/737ee3f52d0a27e26332cde85b533c21fcdc0b09fb716c3f8e522cfaa9c600d4a631dec9fcde179ec9d47cca89017b7848ed4d6ae6b6b78f936c06825b1fcc12
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"glob-parent@npm:^5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "glob-parent@npm:5.1.2"
|
||||
@@ -1708,13 +1608,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ieee754@npm:^1.1.13":
|
||||
version: 1.2.1
|
||||
resolution: "ieee754@npm:1.2.1"
|
||||
checksum: 10c0/b0782ef5e0935b9f12883a2e2aa37baa75da6e66ce6515c168697b42160807d9330de9a32ec1ed73149aea02e0d822e572bca6f1e22bdcbd2149e13b050b17bb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"imurmurhash@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "imurmurhash@npm:0.1.4"
|
||||
@@ -1739,20 +1632,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"inherits@npm:2, inherits@npm:^2.0.3, inherits@npm:^2.0.4":
|
||||
"inherits@npm:2":
|
||||
version: 2.0.4
|
||||
resolution: "inherits@npm:2.0.4"
|
||||
checksum: 10c0/4e531f648b29039fb7426fb94075e6545faa1eb9fe83c29f0b6d9e7263aceb4289d2d4557db0d428188eeb449cc7c5e77b0a0b2c4e248ff2a65933a0dee49ef2
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ini@npm:~1.3.0":
|
||||
version: 1.3.8
|
||||
resolution: "ini@npm:1.3.8"
|
||||
checksum: 10c0/ec93838d2328b619532e4f1ff05df7909760b6f66d9c9e2ded11e5c1897d6f2f9980c54dd638f88654b00919ce31e827040631eab0a3969e4d1abefa0719516a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"ip-address@npm:^9.0.5":
|
||||
version: 9.0.5
|
||||
resolution: "ip-address@npm:9.0.5"
|
||||
@@ -2050,13 +1936,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mimic-response@npm:^3.1.0":
|
||||
version: 3.1.0
|
||||
resolution: "mimic-response@npm:3.1.0"
|
||||
checksum: 10c0/0d6f07ce6e03e9e4445bee655202153bdb8a98d67ee8dc965ac140900d7a2688343e6b4c9a72cfc9ef2f7944dfd76eef4ab2482eb7b293a68b84916bac735362
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimatch@npm:^3.0.4, minimatch@npm:^3.1.1":
|
||||
version: 3.1.2
|
||||
resolution: "minimatch@npm:3.1.2"
|
||||
@@ -2075,13 +1954,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minimist@npm:^1.2.0, minimist@npm:^1.2.3":
|
||||
version: 1.2.8
|
||||
resolution: "minimist@npm:1.2.8"
|
||||
checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"minipass-collect@npm:^2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "minipass-collect@npm:2.0.1"
|
||||
@@ -2166,13 +2038,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp-classic@npm:^0.5.2, mkdirp-classic@npm:^0.5.3":
|
||||
version: 0.5.3
|
||||
resolution: "mkdirp-classic@npm:0.5.3"
|
||||
checksum: 10c0/95371d831d196960ddc3833cc6907e6b8f67ac5501a6582f47dfae5eb0f092e9f8ce88e0d83afcae95d6e2b61a01741ba03714eeafb6f7a6e9dcc158ac85b168
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"mkdirp@npm:^1.0.3":
|
||||
version: 1.0.4
|
||||
resolution: "mkdirp@npm:1.0.4"
|
||||
@@ -2217,13 +2082,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"napi-build-utils@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "napi-build-utils@npm:2.0.0"
|
||||
checksum: 10c0/5833aaeb5cc5c173da47a102efa4680a95842c13e0d9cc70428bd3ee8d96bb2172f8860d2811799b5daa5cbeda779933601492a2028a6a5351c6d0fcf6de83db
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"negotiator@npm:^0.6.3":
|
||||
version: 0.6.3
|
||||
resolution: "negotiator@npm:0.6.3"
|
||||
@@ -2231,24 +2089,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-abi@npm:^3.3.0":
|
||||
version: 3.87.0
|
||||
resolution: "node-abi@npm:3.87.0"
|
||||
dependencies:
|
||||
semver: "npm:^7.3.5"
|
||||
checksum: 10c0/41cfc361edd1b0711d412ca9e1a475180c5b897868bd5583df7ff73e30e6044cc7de307df36c2257203320f17fadf7e82dfdf5a9f6fd510a8578e3fe3ed67ebb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-addon-api@npm:^7.0.0":
|
||||
version: 7.1.1
|
||||
resolution: "node-addon-api@npm:7.1.1"
|
||||
dependencies:
|
||||
node-gyp: "npm:latest"
|
||||
checksum: 10c0/fb32a206276d608037fa1bcd7e9921e177fe992fc610d098aa3128baca3c0050fc1e014fa007e9b3874cf865ddb4f5bd9f43ccb7cbbbe4efaff6a83e920b17e9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"node-gyp@npm:latest":
|
||||
version: 10.1.0
|
||||
resolution: "node-gyp@npm:10.1.0"
|
||||
@@ -2296,7 +2136,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"once@npm:^1.3.0, once@npm:^1.3.1, once@npm:^1.4.0":
|
||||
"once@npm:^1.3.0":
|
||||
version: 1.4.0
|
||||
resolution: "once@npm:1.4.0"
|
||||
dependencies:
|
||||
@@ -2453,28 +2293,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prebuild-install@npm:^7.1.3":
|
||||
version: 7.1.3
|
||||
resolution: "prebuild-install@npm:7.1.3"
|
||||
dependencies:
|
||||
detect-libc: "npm:^2.0.0"
|
||||
expand-template: "npm:^2.0.3"
|
||||
github-from-package: "npm:0.0.0"
|
||||
minimist: "npm:^1.2.3"
|
||||
mkdirp-classic: "npm:^0.5.3"
|
||||
napi-build-utils: "npm:^2.0.0"
|
||||
node-abi: "npm:^3.3.0"
|
||||
pump: "npm:^3.0.0"
|
||||
rc: "npm:^1.2.7"
|
||||
simple-get: "npm:^4.0.0"
|
||||
tar-fs: "npm:^2.0.0"
|
||||
tunnel-agent: "npm:^0.6.0"
|
||||
bin:
|
||||
prebuild-install: bin.js
|
||||
checksum: 10c0/25919a42b52734606a4036ab492d37cfe8b601273d8dfb1fa3c84e141a0a475e7bad3ab848c741d2f810cef892fcf6059b8c7fe5b29f98d30e0c29ad009bedff
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"prettier@npm:^3.3.3":
|
||||
version: 3.3.3
|
||||
resolution: "prettier@npm:3.3.3"
|
||||
@@ -2526,16 +2344,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"pump@npm:^3.0.0":
|
||||
version: 3.0.3
|
||||
resolution: "pump@npm:3.0.3"
|
||||
dependencies:
|
||||
end-of-stream: "npm:^1.1.0"
|
||||
once: "npm:^1.3.1"
|
||||
checksum: 10c0/ada5cdf1d813065bbc99aa2c393b8f6beee73b5de2890a8754c9f488d7323ffd2ca5f5a0943b48934e3fcbd97637d0337369c3c631aeb9614915db629f1c75c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"punycode@npm:^2.1.1, punycode@npm:^2.3.1":
|
||||
version: 2.3.1
|
||||
resolution: "punycode@npm:2.3.1"
|
||||
@@ -2557,20 +2365,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"rc@npm:^1.2.7":
|
||||
version: 1.2.8
|
||||
resolution: "rc@npm:1.2.8"
|
||||
dependencies:
|
||||
deep-extend: "npm:^0.6.0"
|
||||
ini: "npm:~1.3.0"
|
||||
minimist: "npm:^1.2.0"
|
||||
strip-json-comments: "npm:~2.0.1"
|
||||
bin:
|
||||
rc: ./cli.js
|
||||
checksum: 10c0/24a07653150f0d9ac7168e52943cc3cb4b7a22c0e43c7dff3219977c2fdca5a2760a304a029c20811a0e79d351f57d46c9bde216193a0f73978496afc2b85b15
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-is@npm:^18.0.0":
|
||||
version: 18.3.1
|
||||
resolution: "react-is@npm:18.3.1"
|
||||
@@ -2578,17 +2372,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"readable-stream@npm:^3.1.1, readable-stream@npm:^3.4.0":
|
||||
version: 3.6.2
|
||||
resolution: "readable-stream@npm:3.6.2"
|
||||
dependencies:
|
||||
inherits: "npm:^2.0.3"
|
||||
string_decoder: "npm:^1.1.1"
|
||||
util-deprecate: "npm:^1.0.1"
|
||||
checksum: 10c0/e37be5c79c376fdd088a45fa31ea2e423e5d48854be7a22a58869b4e84d25047b193f6acb54f1012331e1bcd667ffb569c01b99d36b0bd59658fb33f513511b7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"requires-port@npm:^1.0.0":
|
||||
version: 1.0.0
|
||||
resolution: "requires-port@npm:1.0.0"
|
||||
@@ -2696,13 +2479,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safe-buffer@npm:^5.0.1, safe-buffer@npm:~5.2.0":
|
||||
version: 5.2.1
|
||||
resolution: "safe-buffer@npm:5.2.1"
|
||||
checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"safer-buffer@npm:>= 2.1.2 < 3.0.0":
|
||||
version: 2.1.2
|
||||
resolution: "safer-buffer@npm:2.1.2"
|
||||
@@ -2758,24 +2534,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-concat@npm:^1.0.0":
|
||||
version: 1.0.1
|
||||
resolution: "simple-concat@npm:1.0.1"
|
||||
checksum: 10c0/62f7508e674414008910b5397c1811941d457dfa0db4fd5aa7fa0409eb02c3609608dfcd7508cace75b3a0bf67a2a77990711e32cd213d2c76f4fd12ee86d776
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"simple-get@npm:^4.0.0":
|
||||
version: 4.0.1
|
||||
resolution: "simple-get@npm:4.0.1"
|
||||
dependencies:
|
||||
decompress-response: "npm:^6.0.0"
|
||||
once: "npm:^1.3.1"
|
||||
simple-concat: "npm:^1.0.0"
|
||||
checksum: 10c0/b0649a581dbca741babb960423248899203165769747142033479a7dc5e77d7b0fced0253c731cd57cf21e31e4d77c9157c3069f4448d558ebc96cf9e1eebcf0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"sirv@npm:^2.0.4":
|
||||
version: 2.0.4
|
||||
resolution: "sirv@npm:2.0.4"
|
||||
@@ -2874,15 +2632,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"string_decoder@npm:^1.1.1":
|
||||
version: 1.3.0
|
||||
resolution: "string_decoder@npm:1.3.0"
|
||||
dependencies:
|
||||
safe-buffer: "npm:~5.2.0"
|
||||
checksum: 10c0/810614ddb030e271cd591935dcd5956b2410dd079d64ff92a1844d6b7588bf992b3e1b69b0f4d34a3e06e0bd73046ac646b5264c1987b20d0601f81ef35d731d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1":
|
||||
version: 6.0.1
|
||||
resolution: "strip-ansi@npm:6.0.1"
|
||||
@@ -2908,13 +2657,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-json-comments@npm:~2.0.1":
|
||||
version: 2.0.1
|
||||
resolution: "strip-json-comments@npm:2.0.1"
|
||||
checksum: 10c0/b509231cbdee45064ff4f9fd73609e2bcc4e84a4d508e9dd0f31f70356473fde18abfb5838c17d56fb236f5a06b102ef115438de0600b749e818a35fbbc48c43
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"strip-literal@npm:^2.0.0":
|
||||
version: 2.1.0
|
||||
resolution: "strip-literal@npm:2.1.0"
|
||||
@@ -2940,31 +2682,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-fs@npm:^2.0.0":
|
||||
version: 2.1.4
|
||||
resolution: "tar-fs@npm:2.1.4"
|
||||
dependencies:
|
||||
chownr: "npm:^1.1.1"
|
||||
mkdirp-classic: "npm:^0.5.2"
|
||||
pump: "npm:^3.0.0"
|
||||
tar-stream: "npm:^2.1.4"
|
||||
checksum: 10c0/decb25acdc6839182c06ec83cba6136205bda1db984e120c8ffd0d80182bc5baa1d916f9b6c5c663ea3f9975b4dd49e3c6bb7b1707cbcdaba4e76042f43ec84c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar-stream@npm:^2.1.4":
|
||||
version: 2.2.0
|
||||
resolution: "tar-stream@npm:2.2.0"
|
||||
dependencies:
|
||||
bl: "npm:^4.0.3"
|
||||
end-of-stream: "npm:^1.4.1"
|
||||
fs-constants: "npm:^1.0.0"
|
||||
inherits: "npm:^2.0.3"
|
||||
readable-stream: "npm:^3.1.1"
|
||||
checksum: 10c0/2f4c910b3ee7196502e1ff015a7ba321ec6ea837667220d7bcb8d0852d51cb04b87f7ae471008a6fb8f5b1a1b5078f62f3a82d30c706f20ada1238ac797e7692
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tar@npm:^6.1.11, tar@npm:^6.1.2":
|
||||
version: 6.2.1
|
||||
resolution: "tar@npm:6.2.1"
|
||||
@@ -3055,15 +2772,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tunnel-agent@npm:^0.6.0":
|
||||
version: 0.6.0
|
||||
resolution: "tunnel-agent@npm:0.6.0"
|
||||
dependencies:
|
||||
safe-buffer: "npm:^5.0.1"
|
||||
checksum: 10c0/4c7a1b813e7beae66fdbf567a65ec6d46313643753d0beefb3c7973d66fcec3a1e7f39759f0a0b4465883499c6dc8b0750ab8b287399af2e583823e40410a17a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"type-detect@npm:^4.0.0, type-detect@npm:^4.0.8":
|
||||
version: 4.0.8
|
||||
resolution: "type-detect@npm:4.0.8"
|
||||
@@ -3120,13 +2828,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"util-deprecate@npm:^1.0.1":
|
||||
version: 1.0.2
|
||||
resolution: "util-deprecate@npm:1.0.2"
|
||||
checksum: 10c0/41a5bdd214df2f6c3ecf8622745e4a366c4adced864bc3c833739791aeeeb1838119af7daed4ba36428114b5c67dcda034a79c882e97e43c03e66a4dd7389942
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:1.6.0":
|
||||
version: 1.6.0
|
||||
resolution: "vite-node@npm:1.6.0"
|
||||
|
||||
@@ -1559,12 +1559,6 @@ msgstr "Old password is incorrect"
|
||||
msgid "feedback.description"
|
||||
msgstr "Description"
|
||||
|
||||
msgid "errors.webgl-context-lost.main-message"
|
||||
msgstr "Oops! The canvas context was lost"
|
||||
|
||||
msgid "errors.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL has stopped working. Please reload the page to reset it"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:122
|
||||
msgid "feedback.description-placeholder"
|
||||
msgstr "Please describe the reason of your feedback"
|
||||
@@ -2527,9 +2521,6 @@ msgstr "Release notes"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Reload file"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Reload page"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
|
||||
@@ -1552,12 +1552,6 @@ msgstr "El email o la contraseña son incorrectos."
|
||||
msgid "errors.wrong-old-password"
|
||||
msgstr "La contraseña anterior no es correcta"
|
||||
|
||||
msgid "errors.webgl-context-lost.main-message"
|
||||
msgstr "Ups! Se ha perdido el contexto del canvas"
|
||||
|
||||
msgid "errors.webgl-context-lost.desc-message"
|
||||
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"
|
||||
|
||||
#: src/app/main/ui/settings/feedback.cljs:120
|
||||
msgid "feedback.description"
|
||||
msgstr "Descripción"
|
||||
@@ -2508,9 +2502,6 @@ msgstr "Notas de versión"
|
||||
msgid "labels.reload-file"
|
||||
msgstr "Recargar archivo"
|
||||
|
||||
msgid "labels.reload-page"
|
||||
msgstr "Recargar página"
|
||||
|
||||
#: src/app/main/ui/workspace/libraries.cljs, src/app/main/ui/dashboard/team.cljs
|
||||
#, unused
|
||||
msgid "labels.remove"
|
||||
|
||||
8
plugins/apps/colors-to-tokens-plugin/wrangler.toml
Normal file
8
plugins/apps/colors-to-tokens-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "color-to-tokens-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/contrast-plugin/wrangler.toml
Normal file
8
plugins/apps/contrast-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "contrast-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/contrast-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/create-palette-plugin/wrangler.toml
Normal file
8
plugins/apps/create-palette-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "create-palette-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/create-palette-plugin" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/icons-plugin/wrangler.toml
Normal file
8
plugins/apps/icons-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "icons-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/icons-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/lorem-ipsum-plugin/wrangler.toml
Normal file
8
plugins/apps/lorem-ipsum-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "lorem-ipsum-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/rename-layers-plugin/wrangler.toml
Normal file
8
plugins/apps/rename-layers-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "rename-layers-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/rename-layers-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/table-plugin/wrangler.toml
Normal file
8
plugins/apps/table-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "table-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/table-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
@@ -23,7 +23,7 @@ use std::collections::HashMap;
|
||||
use utils::uuid_from_u32_quartet;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||||
pub(crate) static mut STATE: Option<Box<State<'static>>> = None;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! with_state_mut {
|
||||
@@ -191,20 +191,6 @@ pub extern "C" fn render_from_cache(_: i32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.set_preview_mode(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_preview() {
|
||||
with_state_mut!(state, {
|
||||
state.render_preview(performance::get_time());
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
@@ -275,27 +261,29 @@ pub extern "C" fn set_view_end() {
|
||||
state.render_state.options.set_fast_mode(false);
|
||||
state.render_state.cancel_animation_frame();
|
||||
|
||||
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
|
||||
// This is critical because we limit tiles to the interest area for optimization
|
||||
let scale = state.render_state.get_scale();
|
||||
state
|
||||
.render_state
|
||||
.tile_viewbox
|
||||
.update(state.render_state.viewbox, scale);
|
||||
|
||||
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
|
||||
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
let zoom_changed = state.render_state.zoom_changed();
|
||||
// Only rebuild tile indices when zoom has changed.
|
||||
// During pan-only operations, shapes stay in the same tiles
|
||||
// because tile_size = 1/scale * TILE_SIZE (depends only on zoom).
|
||||
if zoom_changed {
|
||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||
state.rebuild_tiles();
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
} else {
|
||||
state.rebuild_tiles_shallow();
|
||||
// During pan, we only clear the tile index without
|
||||
// invalidating cached textures, which is more efficient.
|
||||
let _clear_start = performance::begin_timed_log!("clear_tile_index");
|
||||
performance::begin_measure!("set_view_end::clear_tile_index");
|
||||
state.clear_tile_index();
|
||||
performance::end_measure!("set_view_end::clear_tile_index");
|
||||
performance::end_timed_log!("clear_tile_index", _clear_start);
|
||||
}
|
||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
||||
|
||||
state.render_state.sync_cached_viewbox();
|
||||
performance::end_measure!("set_view_end");
|
||||
performance::end_timed_log!("set_view_end", _end_start);
|
||||
#[cfg(feature = "profile-macros")]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,7 +18,7 @@ fn draw_image_fill(
|
||||
}
|
||||
|
||||
let size = image.unwrap().dimensions();
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(surface_id);
|
||||
let canvas = render_state.surfaces.canvas(surface_id);
|
||||
let container = &shape.selrect;
|
||||
let path_transform = shape.to_path_transform();
|
||||
|
||||
|
||||
@@ -40,21 +40,41 @@ pub fn render_with_filter_surface<F>(
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
if let Some((mut surface, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
||||
let canvas = render_state.surfaces.canvas(target_surface);
|
||||
|
||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||
if scale < 1.0 {
|
||||
// The image was rendered at a smaller scale, so we need to scale it back up
|
||||
let scaled_width = bounds.width() * scale;
|
||||
let scaled_height = bounds.height() * scale;
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
||||
|
||||
canvas.save();
|
||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||
canvas.translate((bounds.left * scale, bounds.top * scale));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
skia::Rect::from_xywh(
|
||||
bounds.left * scale,
|
||||
bounds.top * scale,
|
||||
scaled_width,
|
||||
scaled_height,
|
||||
),
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
canvas.restore();
|
||||
} else {
|
||||
canvas.save();
|
||||
canvas.translate((bounds.left, bounds.top));
|
||||
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||
canvas.restore();
|
||||
// No scaling needed, draw normally
|
||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
||||
canvas.draw_image_rect_with_sampling_options(
|
||||
image,
|
||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
||||
bounds,
|
||||
render_state.sampling_options,
|
||||
&skia::Paint::default(),
|
||||
);
|
||||
}
|
||||
true
|
||||
} else {
|
||||
@@ -73,7 +93,7 @@ pub fn render_into_filter_surface<F>(
|
||||
render_state: &mut RenderState,
|
||||
bounds: Rect,
|
||||
draw_fn: F,
|
||||
) -> Option<(skia::Surface, f32)>
|
||||
) -> Option<(skia::Image, f32)>
|
||||
where
|
||||
F: FnOnce(&mut RenderState, SurfaceId),
|
||||
{
|
||||
@@ -109,6 +129,5 @@ where
|
||||
|
||||
render_state.surfaces.canvas(filter_id).restore();
|
||||
|
||||
let filter_surface = render_state.surfaces.surface_clone(filter_id);
|
||||
Some((filter_surface, scale))
|
||||
Some((render_state.surfaces.snapshot(filter_id), scale))
|
||||
}
|
||||
|
||||
@@ -135,7 +135,7 @@ pub fn render_text_shadows(
|
||||
|
||||
let canvas = render_state
|
||||
.surfaces
|
||||
.canvas_and_mark_dirty(surface_id.unwrap_or(SurfaceId::TextDropShadows));
|
||||
.canvas(surface_id.unwrap_or(SurfaceId::TextDropShadows));
|
||||
|
||||
for shadow in shadows {
|
||||
let shadow_layer = SaveLayerRec::default().paint(shadow);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user