mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 15:51:32 -05:00
Compare commits
25 Commits
niwinz-dev
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fda31624c1 | ||
|
|
7f640569bd | ||
|
|
91f1323802 | ||
|
|
dbd4a2366f | ||
|
|
cbb6d098a7 | ||
|
|
b6f5000d1c | ||
|
|
0527124f2f | ||
|
|
faf91ac70d | ||
|
|
9ca76c745f | ||
|
|
9808b6ca57 | ||
|
|
de41cb5488 | ||
|
|
b40ccaf030 | ||
|
|
7d3ac38749 | ||
|
|
8d1bc6c50c | ||
|
|
3112b240a0 | ||
|
|
56fd66b91a | ||
|
|
2a7c24f6fd | ||
|
|
947aa22dee | ||
|
|
1ce0b60e3d | ||
|
|
5209a8b423 | ||
|
|
f4f4f5bbb5 | ||
|
|
ef80901400 | ||
|
|
5306bed548 | ||
|
|
92a319ddd1 | ||
|
|
68a6d4c9a8 |
28
.github/workflows/plugins-deploy-api-doc.yml
vendored
28
.github/workflows/plugins-deploy-api-doc.yml
vendored
@@ -7,11 +7,11 @@ on:
|
|||||||
- staging
|
- staging
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
- "plugins/libs/plugin-types/index.d.ts"
|
- 'plugins/libs/plugin-types/index.d.ts'
|
||||||
- "plugins/libs/plugin-types/REAME.md"
|
- 'plugins/libs/plugin-types/REAME.md'
|
||||||
- "plugins/tools/typedoc.css"
|
- 'plugins/tools/typedoc.css'
|
||||||
- "plugins/CHANGELOG.md"
|
- 'plugins/CHANGELOG.md'
|
||||||
- "plugins/wrangler-penpot-plugins-api-doc.toml"
|
- 'plugins/wrangler-penpot-plugins-api-doc.toml'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
gh_ref:
|
gh_ref:
|
||||||
@@ -86,12 +86,24 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
REF="${{ steps.vars.outputs.gh_ref }}"
|
REF="${{ steps.vars.outputs.gh_ref }}"
|
||||||
case "$REF" in
|
case "$REF" in
|
||||||
main) echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV ;;
|
main)
|
||||||
staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;;
|
echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV
|
||||||
develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;;
|
echo "WORKER_URI=doc.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||||
|
staging)
|
||||||
|
echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV
|
||||||
|
echo "WORKER_URI=doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||||
|
develop)
|
||||||
|
echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV
|
||||||
|
echo "WORKER_URI=doc.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
- name: Set the custom url
|
||||||
|
working-directory: plugins
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" wrangler-penpot-plugins-api-doc.toml
|
||||||
|
|
||||||
- name: Deploy to Cloudflare Workers
|
- name: Deploy to Cloudflare Workers
|
||||||
uses: cloudflare/wrangler-action@v3
|
uses: cloudflare/wrangler-action@v3
|
||||||
with:
|
with:
|
||||||
|
|||||||
123
.github/workflows/plugins-deploy-styles-doc.yml
vendored
Normal file
123
.github/workflows/plugins-deploy-styles-doc.yml
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
name: Plugins/styles-doc deployer
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
paths:
|
||||||
|
- 'plugins/apps/example-styles/**'
|
||||||
|
- 'plugins/libs/plugins-styles/**'
|
||||||
|
- 'plugins/wrangler-penpot-plugins-styles-doc.toml'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
gh_ref:
|
||||||
|
description: 'Name of the branch'
|
||||||
|
type: choice
|
||||||
|
required: true
|
||||||
|
default: 'develop'
|
||||||
|
options:
|
||||||
|
- develop
|
||||||
|
- staging
|
||||||
|
- main
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Extract some useful variables
|
||||||
|
id: vars
|
||||||
|
run: |
|
||||||
|
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ steps.vars.outputs.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 styles
|
||||||
|
working-directory: plugins
|
||||||
|
shell: bash
|
||||||
|
run: npx nx run example-styles:build
|
||||||
|
|
||||||
|
- name: Select Worker name
|
||||||
|
run: |
|
||||||
|
REF="${{ steps.vars.outputs.gh_ref }}"
|
||||||
|
case "$REF" in
|
||||||
|
main)
|
||||||
|
echo "WORKER_NAME=penpot-plugins-styles-doc-pro" >> $GITHUB_ENV
|
||||||
|
echo "WORKER_URI=styles-doc.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||||
|
staging)
|
||||||
|
echo "WORKER_NAME=penpot-plugins-styles-doc-pre" >> $GITHUB_ENV
|
||||||
|
echo "WORKER_URI=styles-doc.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||||
|
develop)
|
||||||
|
echo "WORKER_NAME=penpot-plugins-styles-doc-hourly" >> $GITHUB_ENV
|
||||||
|
echo "WORKER_URI=styles-doc.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" wrangler-penpot-plugins-styles-doc.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 wrangler-penpot-plugins-styles-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 Styles documentation.*
|
||||||
|
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||||
|
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||||
|
@infra
|
||||||
@@ -30,6 +30,7 @@
|
|||||||
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
|
- Fix displaying a hidden user avatar when there is only one more [Taiga #13058](https://tree.taiga.io/project/penpot/issue/13058)
|
||||||
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
|
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
|
||||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||||
|
- Fix boolean operators in menu for boards [Taiga #13174](https://tree.taiga.io/project/penpot/issue/13174)
|
||||||
|
|
||||||
## 2.13.0 (Unreleased)
|
## 2.13.0 (Unreleased)
|
||||||
|
|
||||||
@@ -69,7 +70,8 @@
|
|||||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
- 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 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 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)
|
||||||
|
|
||||||
## 2.12.1
|
## 2.12.1
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,7 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
export PENPOT_NITRATE_SHARED_KEY=super-secret-nitrate-api-key
|
|
||||||
export PENPOT_EXPORTER_SHARED_KEY=super-secret-exporter-api-key
|
|
||||||
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
|
||||||
|
|
||||||
# DEPRECATED: only used for subscriptions
|
|
||||||
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
export PENPOT_MANAGEMENT_API_KEY=super-secret-management-api-key
|
||||||
|
export PENPOT_SECRET_KEY=super-secret-devenv-key
|
||||||
export PENPOT_HOST=devenv
|
export PENPOT_HOST=devenv
|
||||||
export PENPOT_PUBLIC_URI=https://localhost:3449
|
export PENPOT_PUBLIC_URI=https://localhost:3449
|
||||||
|
|
||||||
|
|||||||
@@ -102,8 +102,6 @@
|
|||||||
[:http-server-io-threads {:optional true} ::sm/int]
|
[:http-server-io-threads {:optional true} ::sm/int]
|
||||||
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
[:http-server-max-worker-threads {:optional true} ::sm/int]
|
||||||
|
|
||||||
[:exporter-shared-key {:optional true} :string]
|
|
||||||
[:nitrate-shared-key {:optional true} :string]
|
|
||||||
[:management-api-key {:optional true} :string]
|
[:management-api-key {:optional true} :string]
|
||||||
|
|
||||||
[:telemetry-uri {:optional true} :string]
|
[:telemetry-uri {:optional true} :string]
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
[app.common.time :as ct]
|
[app.common.time :as ct]
|
||||||
[app.config :as cf]
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
|
[app.http.middleware :as mw]
|
||||||
[app.main :as-alias main]
|
[app.main :as-alias main]
|
||||||
[app.rpc.commands.profile :as cmd.profile]
|
[app.rpc.commands.profile :as cmd.profile]
|
||||||
[app.setup :as-alias setup]
|
[app.setup :as-alias setup]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.worker :as-alias wrk]
|
[app.worker :as-alias wrk]
|
||||||
[integrant.core :as ig]
|
[integrant.core :as ig]
|
||||||
[yetti.request :as yreq]
|
|
||||||
[yetti.response :as-alias yres]))
|
[yetti.response :as-alias yres]))
|
||||||
|
|
||||||
;; ---- ROUTES
|
;; ---- ROUTES
|
||||||
@@ -49,40 +49,28 @@
|
|||||||
(fn [cfg request]
|
(fn [cfg request]
|
||||||
(db/tx-run! cfg handler request)))))})
|
(db/tx-run! cfg handler request)))))})
|
||||||
|
|
||||||
(def ^:private shared-key-auth
|
|
||||||
{:name ::shared-key-auth
|
|
||||||
:compile
|
|
||||||
(fn [_ _]
|
|
||||||
(fn [handler key]
|
|
||||||
(if key
|
|
||||||
(fn [request]
|
|
||||||
(if-let [key' (yreq/get-header request "x-shared-key")]
|
|
||||||
(if (= key key')
|
|
||||||
(handler request)
|
|
||||||
{::yres/status 403})
|
|
||||||
{::yres/status 403}))
|
|
||||||
(fn [_ _]
|
|
||||||
{::yres/status 403}))))})
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ cfg]
|
[_ {:keys [::setup/props] :as cfg}]
|
||||||
|
|
||||||
["" {:middleware [[shared-key-auth (cf/get :management-api-key)]
|
(let [management-key (or (cf/get :management-api-key)
|
||||||
[default-system cfg]
|
(get props :management-key))]
|
||||||
[transaction]]}
|
|
||||||
["/authenticate"
|
|
||||||
{:handler authenticate
|
|
||||||
:allowed-methods #{:post}}]
|
|
||||||
|
|
||||||
["/get-customer"
|
["" {:middleware [[mw/shared-key-auth management-key]
|
||||||
{:handler get-customer
|
[default-system cfg]
|
||||||
:transaction true
|
[transaction]]}
|
||||||
:allowed-methods #{:post}}]
|
["/authenticate"
|
||||||
|
{:handler authenticate
|
||||||
|
:allowed-methods #{:post}}]
|
||||||
|
|
||||||
["/update-customer"
|
["/get-customer"
|
||||||
{:handler update-customer
|
{:handler get-customer
|
||||||
:allowed-methods #{:post}
|
:transaction true
|
||||||
:transaction true}]])
|
:allowed-methods #{:post}}]
|
||||||
|
|
||||||
|
["/update-customer"
|
||||||
|
{:handler update-customer
|
||||||
|
:allowed-methods #{:post}
|
||||||
|
:transaction true}]]))
|
||||||
|
|
||||||
;; ---- HELPERS
|
;; ---- HELPERS
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
[app.http.errors :as errors]
|
[app.http.errors :as errors]
|
||||||
[app.tokens :as tokens]
|
[app.tokens :as tokens]
|
||||||
[app.util.pointer-map :as pmap]
|
[app.util.pointer-map :as pmap]
|
||||||
|
[buddy.core.codecs :as bc]
|
||||||
[cuerdas.core :as str]
|
[cuerdas.core :as str]
|
||||||
[yetti.adapter :as yt]
|
[yetti.adapter :as yt]
|
||||||
[yetti.middleware :as ymw]
|
[yetti.middleware :as ymw]
|
||||||
@@ -300,20 +301,16 @@
|
|||||||
:compile (constantly wrap-auth)})
|
:compile (constantly wrap-auth)})
|
||||||
|
|
||||||
(defn- wrap-shared-key-auth
|
(defn- wrap-shared-key-auth
|
||||||
[handler keys]
|
[handler shared-key]
|
||||||
(if (seq keys)
|
(if shared-key
|
||||||
(fn [request]
|
(let [shared-key (if (string? shared-key)
|
||||||
(if-let [[key-id key] (some-> (yreq/get-header request "x-shared-key")
|
shared-key
|
||||||
(str/split #"\s+" 2))]
|
(bc/bytes->b64-str shared-key true))]
|
||||||
(let [key-id (str/lower key-id)]
|
(fn [request]
|
||||||
(if (and (string? key)
|
(let [key (yreq/get-header request "x-shared-key")]
|
||||||
(contains? keys key-id)
|
(if (= key shared-key)
|
||||||
(= key (get keys key-id)))
|
(handler (assoc request ::http/auth-with-shared-key true))
|
||||||
(-> request
|
{::yres/status 403}))))
|
||||||
(assoc ::http/auth-key-id key-id)
|
|
||||||
(handler))
|
|
||||||
{::yres/status 403}))
|
|
||||||
{::yres/status 403}))
|
|
||||||
(fn [_ _]
|
(fn [_ _]
|
||||||
{::yres/status 403})))
|
{::yres/status 403})))
|
||||||
|
|
||||||
|
|||||||
@@ -140,14 +140,10 @@
|
|||||||
client-version (get-client-version request)
|
client-version (get-client-version request)
|
||||||
client-user-agent (get-client-user-agent request)
|
client-user-agent (get-client-user-agent request)
|
||||||
session-id (get-external-session-id request)
|
session-id (get-external-session-id request)
|
||||||
key-id (::http/auth-key-id request)
|
token-id (::actoken/id request)]
|
||||||
token-id (::actoken/id request)
|
|
||||||
token-type (::actoken/type request)]
|
|
||||||
(d/without-nils
|
(d/without-nils
|
||||||
{:external-session-id session-id
|
{:external-session-id session-id
|
||||||
:initiator (or key-id "app")
|
|
||||||
:access-token-id (some-> token-id str)
|
:access-token-id (some-> token-id str)
|
||||||
:access-token-type (some-> token-type str)
|
|
||||||
:client-event-origin client-event-origin
|
:client-event-origin client-event-origin
|
||||||
:client-user-agent client-user-agent
|
:client-user-agent client-user-agent
|
||||||
:client-version client-version
|
:client-version client-version
|
||||||
|
|||||||
@@ -275,7 +275,8 @@
|
|||||||
::email/whitelist (ig/ref ::email/whitelist)}
|
::email/whitelist (ig/ref ::email/whitelist)}
|
||||||
|
|
||||||
::mgmt/routes
|
::mgmt/routes
|
||||||
{::db/pool (ig/ref ::db/pool)}
|
{::db/pool (ig/ref ::db/pool)
|
||||||
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
:app.http/router
|
:app.http/router
|
||||||
{::session/manager (ig/ref ::session/manager)
|
{::session/manager (ig/ref ::session/manager)
|
||||||
@@ -340,8 +341,7 @@
|
|||||||
::email/whitelist (ig/ref ::email/whitelist)}
|
::email/whitelist (ig/ref ::email/whitelist)}
|
||||||
|
|
||||||
:app.nitrate/client
|
:app.nitrate/client
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)}
|
||||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
|
||||||
|
|
||||||
:app.rpc/management-methods
|
:app.rpc/management-methods
|
||||||
{::http.client/client (ig/ref ::http.client/client)
|
{::http.client/client (ig/ref ::http.client/client)
|
||||||
@@ -357,13 +357,13 @@
|
|||||||
::setup/props (ig/ref ::setup/props)}
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
::rpc/routes
|
::rpc/routes
|
||||||
{::rpc/methods (ig/ref :app.rpc/methods)
|
{::rpc/methods (ig/ref :app.rpc/methods)
|
||||||
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
::rpc/management-methods (ig/ref :app.rpc/management-methods)
|
||||||
|
|
||||||
;; FIXME: revisit if db/pool is necessary here
|
;; FIXME: revisit if db/pool is necessary here
|
||||||
::db/pool (ig/ref ::db/pool)
|
::db/pool (ig/ref ::db/pool)
|
||||||
::session/manager (ig/ref ::session/manager)
|
::session/manager (ig/ref ::session/manager)
|
||||||
::setup/shared-keys (ig/ref ::setup/shared-keys)}
|
::setup/props (ig/ref ::setup/props)}
|
||||||
|
|
||||||
::wrk/registry
|
::wrk/registry
|
||||||
{::mtx/metrics (ig/ref ::mtx/metrics)
|
{::mtx/metrics (ig/ref ::mtx/metrics)
|
||||||
@@ -451,11 +451,6 @@
|
|||||||
;; module requires the migrations to run before initialize.
|
;; module requires the migrations to run before initialize.
|
||||||
::migrations (ig/ref :app.migrations/migrations)}
|
::migrations (ig/ref :app.migrations/migrations)}
|
||||||
|
|
||||||
::setup/shared-keys
|
|
||||||
{::setup/props (ig/ref ::setup/props)
|
|
||||||
:nitrate (cf/get :nitrate-shared-key)
|
|
||||||
:exporter (cf/get :exporter-shared-key)}
|
|
||||||
|
|
||||||
::setup/clock
|
::setup/clock
|
||||||
{}
|
{}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
;; 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.nitrate
|
(ns app.nitrate
|
||||||
"Module that make calls to the external nitrate aplication"
|
"Module that make calls to the external nitrate aplication"
|
||||||
(:require
|
(:require
|
||||||
@@ -11,17 +17,18 @@
|
|||||||
[clojure.core :as c]
|
[clojure.core :as c]
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; HELPERS
|
;; HELPERS
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defn- request-builder
|
(defn- request-builder
|
||||||
[cfg method uri shared-key profile-id]
|
[cfg method uri management-key profile-id]
|
||||||
(fn []
|
(fn []
|
||||||
(http/req! cfg {:method method
|
(http/req! cfg {:method method
|
||||||
:headers {"content-type" "application/json"
|
:headers {"content-type" "application/json"
|
||||||
"accept" "application/json"
|
"accept" "application/json"
|
||||||
"x-shared-key" shared-key
|
"x-shared-key" management-key
|
||||||
"x-profile-id" (str profile-id)}
|
"x-profile-id" (str profile-id)}
|
||||||
:uri uri
|
:uri uri
|
||||||
:version :http1.1})))
|
:version :http1.1})))
|
||||||
@@ -47,9 +54,9 @@
|
|||||||
|
|
||||||
(defn- with-validate [handler uri schema]
|
(defn- with-validate [handler uri schema]
|
||||||
(fn []
|
(fn []
|
||||||
(let [coercer-http (sm/coercer schema
|
(let [coercer-http (sm/coercer schema
|
||||||
:type :validation
|
:type :validation
|
||||||
:hint (str "invalid data received calling " uri))]
|
:hint (str "invalid data received calling " uri))]
|
||||||
(try
|
(try
|
||||||
(coercer-http (-> (handler) :body json/decode))
|
(coercer-http (-> (handler) :body json/decode))
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
@@ -58,9 +65,8 @@
|
|||||||
nil)))))
|
nil)))))
|
||||||
|
|
||||||
(defn- request-to-nitrate
|
(defn- request-to-nitrate
|
||||||
[cfg method uri schema {:keys [::rpc/profile-id] :as params}]
|
[{:keys [::management-key] :as cfg} method uri schema {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [shared-key (-> cfg ::setup/shared-keys :nitrate)
|
(let [full-http-call (-> (request-builder cfg method uri management-key profile-id)
|
||||||
full-http-call (-> (request-builder cfg method uri shared-key profile-id)
|
|
||||||
(with-retries 3)
|
(with-retries 3)
|
||||||
(with-validate uri schema))]
|
(with-validate uri schema))]
|
||||||
(full-http-call)))
|
(full-http-call)))
|
||||||
@@ -97,15 +103,26 @@
|
|||||||
(let [baseuri (cf/get :nitrate-backend-uri)]
|
(let [baseuri (cf/get :nitrate-backend-uri)]
|
||||||
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
(request-to-nitrate cfg :get (str baseuri "/api/users/" (str profile-id)) schema:user params)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; INITIALIZATION
|
;; INITIALIZATION
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
(defmethod ig/init-key ::client
|
(defmethod ig/init-key ::client
|
||||||
[_ cfg]
|
[_ {:keys [::setup/props] :as cfg}]
|
||||||
(when (contains? cf/flags :nitrate)
|
(if (contains? cf/flags :nitrate)
|
||||||
{:get-team-org (partial get-team-org cfg)
|
(let [management-key (or (cf/get :management-api-key)
|
||||||
:is-valid-user (partial is-valid-user cfg)}))
|
(get props :management-key))
|
||||||
|
cfg (assoc cfg ::management-key management-key)]
|
||||||
|
{:get-team-org (partial get-team-org cfg)
|
||||||
|
:is-valid-user (partial is-valid-user cfg)})
|
||||||
|
{}))
|
||||||
|
|
||||||
|
(defmethod ig/halt-key! ::client
|
||||||
|
[_ {:keys []}]
|
||||||
|
(do :stuff))
|
||||||
|
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
;; UTILS
|
;; UTILS
|
||||||
|
|||||||
@@ -92,11 +92,11 @@
|
|||||||
(fn [{:keys [params path-params method] :as request}]
|
(fn [{:keys [params path-params method] :as request}]
|
||||||
(let [handler-name (:type path-params)
|
(let [handler-name (:type path-params)
|
||||||
etag (yreq/get-header request "if-none-match")
|
etag (yreq/get-header request "if-none-match")
|
||||||
|
|
||||||
key-id (get request ::http/auth-key-id)
|
|
||||||
profile-id (or (::session/profile-id request)
|
profile-id (or (::session/profile-id request)
|
||||||
(::actoken/profile-id request)
|
(::actoken/profile-id request)
|
||||||
(if key-id uuid/zero nil))
|
(if (::http/auth-with-shared-key request)
|
||||||
|
uuid/zero
|
||||||
|
nil))
|
||||||
|
|
||||||
ip-addr (inet/parse-request request)
|
ip-addr (inet/parse-request request)
|
||||||
|
|
||||||
@@ -298,12 +298,11 @@
|
|||||||
|
|
||||||
(defn- resolve-management-methods
|
(defn- resolve-management-methods
|
||||||
[cfg]
|
[cfg]
|
||||||
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)
|
(let [cfg (assoc cfg ::type "management" ::metrics-id :rpc-management-timing)]
|
||||||
mods (cond->> (list 'app.rpc.management.exporter)
|
(->> (sv/scan-ns
|
||||||
(contains? cf/flags :nitrate)
|
'app.rpc.management.subscription
|
||||||
(cons 'app.rpc.management.nitrate))]
|
'app.rpc.management.nitrate
|
||||||
|
'app.rpc.management.exporter)
|
||||||
(->> (apply sv/scan-ns mods)
|
|
||||||
(map (partial process-method cfg "management" wrap-management))
|
(map (partial process-method cfg "management" wrap-management))
|
||||||
(into {}))))
|
(into {}))))
|
||||||
|
|
||||||
@@ -347,20 +346,23 @@
|
|||||||
|
|
||||||
(defmethod ig/assert-key ::routes
|
(defmethod ig/assert-key ::routes
|
||||||
[_ params]
|
[_ params]
|
||||||
(assert (map? (::setup/shared-keys params)))
|
|
||||||
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
(assert (db/pool? (::db/pool params)) "expect valid database pool")
|
||||||
|
(assert (some? (::setup/props params)))
|
||||||
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
(assert (session/manager? (::session/manager params)) "expect valid session manager")
|
||||||
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
(assert (valid-methods? (::methods params)) "expect valid methods map")
|
||||||
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
(assert (valid-methods? (::management-methods params)) "expect valid methods map"))
|
||||||
|
|
||||||
(defmethod ig/init-key ::routes
|
(defmethod ig/init-key ::routes
|
||||||
[_ {:keys [::methods ::management-methods ::setup/shared-keys] :as cfg}]
|
[_ {:keys [::methods ::management-methods ::setup/props] :as cfg}]
|
||||||
|
|
||||||
|
(let [public-uri (cf/get :public-uri)
|
||||||
|
management-key (or (cf/get :management-api-key)
|
||||||
|
(get props :management-key))]
|
||||||
|
|
||||||
(let [public-uri (cf/get :public-uri)]
|
|
||||||
["/api"
|
["/api"
|
||||||
["/management"
|
["/management"
|
||||||
["/methods/:type"
|
["/methods/:type"
|
||||||
{:middleware [[mw/shared-key-auth shared-keys]
|
{:middleware [[mw/shared-key-auth management-key]
|
||||||
[session/authz cfg]]
|
[session/authz cfg]]
|
||||||
:handler (make-rpc-handler management-methods)}]
|
:handler (make-rpc-handler management-methods)}]
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,15 @@
|
|||||||
;; Copyright (c) KALEIDOS INC
|
;; Copyright (c) KALEIDOS INC
|
||||||
|
|
||||||
(ns app.rpc.management.nitrate
|
(ns app.rpc.management.nitrate
|
||||||
"Internal Nitrate HTTP RPC API. Provides authenticated access to
|
"Internal Nitrate HTTP API.
|
||||||
organization management and token validation endpoints."
|
Provides authenticated access to organization management and token validation endpoints.
|
||||||
|
All requests must include a valid shared key token in the `x-shared-key` header, and
|
||||||
|
a cookie `auth-token` with the user token.
|
||||||
|
They will return `401 Unauthorized` if the shared key or user token are invalid."
|
||||||
(:require
|
(:require
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.types.profile :refer [schema:profile]]
|
|
||||||
[app.common.types.team :refer [schema:team]]
|
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.db :as db]
|
[app.db :as db]
|
||||||
[app.msgbus :as mbus]
|
[app.msgbus :as mbus]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
@@ -21,14 +23,22 @@
|
|||||||
[app.util.services :as sv]))
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
;; ---- API: authenticate
|
;; ---- API: authenticate
|
||||||
|
(def ^:private schema:profile
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:email :string]
|
||||||
|
[:photo-url :string]])
|
||||||
|
|
||||||
(sv/defmethod ::authenticate
|
(sv/defmethod ::authenticate
|
||||||
"Authenticate the current user"
|
"Authenticate an user
|
||||||
{::doc/added "2.14"
|
@api GET /authenticate
|
||||||
::sm/params [:map]
|
@returns
|
||||||
|
200 OK: Returns the authenticated user."
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/result schema:profile}
|
::sm/result schema:profile}
|
||||||
[cfg {:keys [::rpc/profile-id] :as params}]
|
[cfg {:keys [::rpc/profile-id] :as params}]
|
||||||
(let [profile (profile/get-profile cfg profile-id)]
|
(let [profile (profile/get-profile cfg profile-id)]
|
||||||
{:id (get profile :id)
|
{:id (get profile :id)
|
||||||
:name (get profile :fullname)
|
:name (get profile :fullname)
|
||||||
:email (get profile :email)
|
:email (get profile :email)
|
||||||
@@ -41,22 +51,30 @@
|
|||||||
FROM team AS t
|
FROM team AS t
|
||||||
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
JOIN team_profile_rel AS tpr ON t.id = tpr.team_id
|
||||||
WHERE tpr.profile_id = ?
|
WHERE tpr.profile_id = ?
|
||||||
AND tpr.is_owner IS TRUE
|
AND tpr.is_owner = 't'
|
||||||
AND t.is_default IS FALSE
|
AND t.is_default = 'f'
|
||||||
AND t.deleted_at IS NULL;")
|
AND t.deleted_at is null;")
|
||||||
|
|
||||||
|
(def ^:private schema:team
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]])
|
||||||
|
|
||||||
(def ^:private schema:get-teams-result
|
(def ^:private schema:get-teams-result
|
||||||
[:vector schema:team])
|
[:vector schema:team])
|
||||||
|
|
||||||
(sv/defmethod ::get-teams
|
(sv/defmethod ::get-teams
|
||||||
"List teams for which current user is owner"
|
"List teams for which current user is owner.
|
||||||
{::doc/added "2.14"
|
@api GET /get-teams
|
||||||
::sm/params [:map]
|
@returns
|
||||||
|
200 OK: Returns the list of teams for the user."
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/result schema:get-teams-result}
|
::sm/result schema:get-teams-result}
|
||||||
[cfg {:keys [::rpc/profile-id]}]
|
[cfg {:keys [::rpc/profile-id]}]
|
||||||
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
(when (contains? cf/flags :nitrate)
|
||||||
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
(let [current-user-id (-> (profile/get-profile cfg profile-id) :id)]
|
||||||
(map #(select-keys % [:id :name])))))
|
(->> (db/exec! cfg [sql:get-teams current-user-id])
|
||||||
|
(map #(select-keys % [:id :name]))))))
|
||||||
|
|
||||||
;; ---- API: notify-team-change
|
;; ---- API: notify-team-change
|
||||||
|
|
||||||
@@ -65,18 +83,30 @@
|
|||||||
[:id ::sm/uuid]
|
[:id ::sm/uuid]
|
||||||
[:organization-id ::sm/text]])
|
[:organization-id ::sm/text]])
|
||||||
|
|
||||||
|
|
||||||
(sv/defmethod ::notify-team-change
|
(sv/defmethod ::notify-team-change
|
||||||
"Notify to Penpot a team change from nitrate"
|
"Notify to Penpot a team change from nitrate
|
||||||
{::doc/added "2.14"
|
@api POST /notify-team-change
|
||||||
|
@returns
|
||||||
|
200 OK"
|
||||||
|
{::doc/added "2.12"
|
||||||
::sm/params schema:notify-team-change
|
::sm/params schema:notify-team-change
|
||||||
::rpc/auth false}
|
::rpc/auth false}
|
||||||
[cfg {:keys [id organization-id organization-name]}]
|
[cfg {:keys [id organization-id organization-name]}]
|
||||||
(let [msgbus (::mbus/msgbus cfg)]
|
(when (contains? cf/flags :nitrate)
|
||||||
(mbus/pub! msgbus
|
(let [msgbus (::mbus/msgbus cfg)]
|
||||||
;;TODO There is a bug on dashboard with teams notifications.
|
(mbus/pub! msgbus
|
||||||
;;For now we send it to uuid/zero instead of team-id
|
;;TODO There is a bug on dashboard with teams notifications.
|
||||||
:topic uuid/zero
|
;;For now we send it to uuid/zero instead of team-id
|
||||||
:message {:type :team-org-change
|
:topic uuid/zero
|
||||||
:team-id id
|
:message {:type :team-org-change
|
||||||
:organization-id organization-id
|
:team-id id
|
||||||
:organization-name organization-name})))
|
:organization-id organization-id
|
||||||
|
:organization-name organization-name}))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
183
backend/src/app/rpc/management/subscription.clj
Normal file
183
backend/src/app/rpc/management/subscription.clj
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
;; 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.rpc.management.subscription
|
||||||
|
(:require
|
||||||
|
[app.common.logging :as l]
|
||||||
|
[app.common.schema :as sm]
|
||||||
|
[app.common.schema.generators :as sg]
|
||||||
|
[app.common.time :as ct]
|
||||||
|
[app.db :as db]
|
||||||
|
[app.rpc :as-alias rpc]
|
||||||
|
[app.rpc.commands.profile :as profile]
|
||||||
|
[app.rpc.doc :as doc]
|
||||||
|
[app.util.services :as sv]))
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: AUTHENTICATE
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
schema:authenticate-params
|
||||||
|
[:map {:title "authenticate-params"}])
|
||||||
|
|
||||||
|
(def ^:private
|
||||||
|
schema:authenticate-result
|
||||||
|
[:map {:title "authenticate-result"}
|
||||||
|
[:profile-id ::sm/uuid]])
|
||||||
|
|
||||||
|
(sv/defmethod ::auth
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:authenticate-params
|
||||||
|
::sm/result schema:authenticate-result}
|
||||||
|
[_ {:keys [::rpc/profile-id]}]
|
||||||
|
{:profile-id profile-id})
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: GET-CUSTOMER
|
||||||
|
|
||||||
|
;; FIXME: move to app.common.time
|
||||||
|
(def ^:private schema:timestamp
|
||||||
|
(sm/type-schema
|
||||||
|
{:type ::timestamp
|
||||||
|
:pred ct/inst?
|
||||||
|
:type-properties
|
||||||
|
{:title "inst"
|
||||||
|
:description "The same as :app.common.time/inst but encodes to epoch"
|
||||||
|
:error/message "should be an instant"
|
||||||
|
:gen/gen (->> (sg/small-int)
|
||||||
|
(sg/fmap (fn [v] (ct/inst v))))
|
||||||
|
:decode/string #(some-> % ct/inst)
|
||||||
|
:encode/string #(some-> % inst-ms)
|
||||||
|
:decode/json #(some-> % ct/inst)
|
||||||
|
:encode/json #(some-> % inst-ms)}}))
|
||||||
|
|
||||||
|
(def ^:private schema:subscription
|
||||||
|
[:map {:title "Subscription"}
|
||||||
|
[:id ::sm/text]
|
||||||
|
[:customer-id ::sm/text]
|
||||||
|
[:type [:enum
|
||||||
|
"unlimited"
|
||||||
|
"professional"
|
||||||
|
"enterprise"]]
|
||||||
|
[:status [:enum
|
||||||
|
"active"
|
||||||
|
"canceled"
|
||||||
|
"incomplete"
|
||||||
|
"incomplete_expired"
|
||||||
|
"past_due"
|
||||||
|
"paused"
|
||||||
|
"trialing"
|
||||||
|
"unpaid"]]
|
||||||
|
|
||||||
|
[:billing-period [:enum
|
||||||
|
"month"
|
||||||
|
"day"
|
||||||
|
"week"
|
||||||
|
"year"]]
|
||||||
|
[:quantity :int]
|
||||||
|
[:description [:maybe ::sm/text]]
|
||||||
|
[:created-at schema:timestamp]
|
||||||
|
[:start-date [:maybe schema:timestamp]]
|
||||||
|
[:ended-at [:maybe schema:timestamp]]
|
||||||
|
[:trial-end [:maybe schema:timestamp]]
|
||||||
|
[:trial-start [:maybe schema:timestamp]]
|
||||||
|
[:cancel-at [:maybe schema:timestamp]]
|
||||||
|
[:canceled-at [:maybe schema:timestamp]]
|
||||||
|
[:current-period-end [:maybe schema:timestamp]]
|
||||||
|
[:current-period-start [:maybe schema:timestamp]]
|
||||||
|
[:cancel-at-period-end :boolean]
|
||||||
|
|
||||||
|
[:cancellation-details
|
||||||
|
[:map {:title "CancellationDetails"}
|
||||||
|
[:comment [:maybe ::sm/text]]
|
||||||
|
[:reason [:maybe ::sm/text]]
|
||||||
|
[:feedback [:maybe
|
||||||
|
[:enum
|
||||||
|
"customer_service"
|
||||||
|
"low_quality"
|
||||||
|
"missing_feature"
|
||||||
|
"other"
|
||||||
|
"switched_service"
|
||||||
|
"too_complex"
|
||||||
|
"too_expensive"
|
||||||
|
"unused"]]]]]])
|
||||||
|
|
||||||
|
(def ^:private sql:get-customer-slots
|
||||||
|
"WITH teams AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
tpr.profile_id AS profile_id
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.is_owner IS true
|
||||||
|
AND tpr.profile_id = ?
|
||||||
|
), teams_with_slots AS (
|
||||||
|
SELECT tpr.team_id AS id,
|
||||||
|
count(*) AS total
|
||||||
|
FROM team_profile_rel AS tpr
|
||||||
|
WHERE tpr.team_id IN (SELECT id FROM teams)
|
||||||
|
AND tpr.can_edit IS true
|
||||||
|
GROUP BY 1
|
||||||
|
ORDER BY 2
|
||||||
|
)
|
||||||
|
SELECT max(total) AS total FROM teams_with_slots;")
|
||||||
|
|
||||||
|
(defn- get-customer-slots
|
||||||
|
[cfg profile-id]
|
||||||
|
(let [result (db/exec-one! cfg [sql:get-customer-slots profile-id])]
|
||||||
|
(:total result)))
|
||||||
|
|
||||||
|
(def ^:private schema:get-customer-params
|
||||||
|
[:map])
|
||||||
|
|
||||||
|
(def ^:private schema:get-customer-result
|
||||||
|
[:map
|
||||||
|
[:id ::sm/uuid]
|
||||||
|
[:name :string]
|
||||||
|
[:num-editors ::sm/int]
|
||||||
|
[:subscription {:optional true} schema:subscription]])
|
||||||
|
|
||||||
|
(sv/defmethod ::get-customer
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:get-customer-params
|
||||||
|
::sm/result schema:get-customer-result}
|
||||||
|
[cfg {:keys [::rpc/profile-id]}]
|
||||||
|
(let [profile (profile/get-profile cfg profile-id)]
|
||||||
|
{:id (get profile :id)
|
||||||
|
:name (get profile :fullname)
|
||||||
|
:email (get profile :email)
|
||||||
|
:num-editors (get-customer-slots cfg profile-id)
|
||||||
|
:subscription (-> profile :props :subscription)}))
|
||||||
|
|
||||||
|
|
||||||
|
;; ---- RPC METHOD: GET-CUSTOMER
|
||||||
|
|
||||||
|
(def ^:private schema:update-customer-params
|
||||||
|
[:map
|
||||||
|
[:subscription [:maybe schema:subscription]]])
|
||||||
|
|
||||||
|
(def ^:private schema:update-customer-result
|
||||||
|
[:map])
|
||||||
|
|
||||||
|
(sv/defmethod ::update-customer
|
||||||
|
{::doc/added "2.12"
|
||||||
|
::sm/params schema:update-customer-params
|
||||||
|
::sm/result schema:update-customer-result}
|
||||||
|
[cfg {:keys [::rpc/profile-id subscription]}]
|
||||||
|
(let [{:keys [props] :as profile}
|
||||||
|
(profile/get-profile cfg profile-id ::db/for-update true)
|
||||||
|
|
||||||
|
props
|
||||||
|
(assoc props :subscription subscription)]
|
||||||
|
|
||||||
|
(l/dbg :hint "update customer"
|
||||||
|
:profile-id (str profile-id)
|
||||||
|
:subscription-type (get subscription :type)
|
||||||
|
:subscription-status (get subscription :status)
|
||||||
|
:subscription-quantity (get subscription :quantity))
|
||||||
|
|
||||||
|
(db/update! cfg :profile
|
||||||
|
{:props (db/tjson props)}
|
||||||
|
{:id profile-id}
|
||||||
|
{::db/return-keys false})
|
||||||
|
|
||||||
|
nil))
|
||||||
@@ -17,7 +17,6 @@
|
|||||||
[app.setup.templates]
|
[app.setup.templates]
|
||||||
[buddy.core.codecs :as bc]
|
[buddy.core.codecs :as bc]
|
||||||
[buddy.core.nonce :as bn]
|
[buddy.core.nonce :as bn]
|
||||||
[cuerdas.core :as str]
|
|
||||||
[integrant.core :as ig]))
|
[integrant.core :as ig]))
|
||||||
|
|
||||||
(defn- generate-random-key
|
(defn- generate-random-key
|
||||||
@@ -89,38 +88,7 @@
|
|||||||
(-> (get-all-props conn)
|
(-> (get-all-props conn)
|
||||||
(assoc :secret-key secret)
|
(assoc :secret-key secret)
|
||||||
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
(assoc :tokens-key (keys/derive secret :salt "tokens"))
|
||||||
|
(assoc :management-key (keys/derive secret :salt "management"))
|
||||||
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
(update :instance-id handle-instance-id conn (db/read-only? pool)))))))
|
||||||
|
|
||||||
(sm/register! ::props [:map-of :keyword ::sm/any])
|
(sm/register! ::props [:map-of :keyword ::sm/any])
|
||||||
|
|
||||||
|
|
||||||
(defmethod ig/init-key ::shared-keys
|
|
||||||
[_ {:keys [::props] :as cfg}]
|
|
||||||
(let [secret (get props :secret-key)]
|
|
||||||
(d/without-nils
|
|
||||||
{"exporter"
|
|
||||||
(let [key (or (get cfg :exporter)
|
|
||||||
(-> (keys/derive secret :salt "exporter")
|
|
||||||
(bc/bytes->b64-str true)))]
|
|
||||||
(if (or (str/empty? key)
|
|
||||||
(str/blank? key))
|
|
||||||
(do
|
|
||||||
(l/wrn :hint "exporter key is disabled because empty string found")
|
|
||||||
nil)
|
|
||||||
(do
|
|
||||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
|
||||||
key)))
|
|
||||||
|
|
||||||
"nitrate"
|
|
||||||
(let [key (or (get cfg :nitrate)
|
|
||||||
(-> (keys/derive secret :salt "nitrate")
|
|
||||||
(bc/bytes->b64-str true)))]
|
|
||||||
(if (or (str/empty? key)
|
|
||||||
(str/blank? key))
|
|
||||||
(do
|
|
||||||
(l/wrn :hint "nitrate key is disabled because empty string found")
|
|
||||||
nil)
|
|
||||||
(do
|
|
||||||
(l/inf :hint "nitrate key initialized" :key (d/obfuscate-string key))
|
|
||||||
key)))})))
|
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@
|
|||||||
(t/deftest shared-key-auth
|
(t/deftest shared-key-auth
|
||||||
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
(let [handler (#'app.http.middleware/wrap-shared-key-auth
|
||||||
(fn [req] {::yres/status 200})
|
(fn [req] {::yres/status 200})
|
||||||
{"test1" "secret-key"})]
|
"secret-key")]
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {} {}))]
|
(let [response (handler (->DummyRequest {} {}))]
|
||||||
(t/is (= 403 (::yres/status response))))
|
(t/is (= 403 (::yres/status response))))
|
||||||
@@ -95,9 +95,6 @@
|
|||||||
(t/is (= 403 (::yres/status response))))
|
(t/is (= 403 (::yres/status response))))
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
(let [response (handler (->DummyRequest {"x-shared-key" "secret-key"} {}))]
|
||||||
(t/is (= 403 (::yres/status response))))
|
|
||||||
|
|
||||||
(let [response (handler (->DummyRequest {"x-shared-key" "test1 secret-key"} {}))]
|
|
||||||
(t/is (= 200 (::yres/status response))))))
|
(t/is (= 200 (::yres/status response))))))
|
||||||
|
|
||||||
(t/deftest access-token-authz
|
(t/deftest access-token-authz
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
@@ -124,33 +124,51 @@
|
|||||||
|
|
||||||
(defn adjust-to-viewport
|
(defn adjust-to-viewport
|
||||||
([viewport srect] (adjust-to-viewport viewport srect nil))
|
([viewport srect] (adjust-to-viewport viewport srect nil))
|
||||||
([viewport srect {:keys [padding] :or {padding 0}}]
|
([viewport srect {:keys [padding min-zoom] :or {padding 0 min-zoom nil}}]
|
||||||
(let [gprop (/ (:width viewport)
|
(let [gprop (/ (:width viewport)
|
||||||
(:height viewport))
|
(:height viewport))
|
||||||
srect (-> srect
|
srect-padded (-> srect
|
||||||
(update :x #(- % padding))
|
(update :x #(- % padding))
|
||||||
(update :y #(- % padding))
|
(update :y #(- % padding))
|
||||||
(update :width #(+ % padding padding))
|
(update :width #(+ % padding padding))
|
||||||
(update :height #(+ % padding padding)))
|
(update :height #(+ % padding padding)))
|
||||||
width (:width srect)
|
width (:width srect-padded)
|
||||||
height (:height srect)
|
height (:height srect-padded)
|
||||||
lprop (/ width height)]
|
lprop (/ width height)
|
||||||
(cond
|
adjusted-rect
|
||||||
(> gprop lprop)
|
(cond
|
||||||
(let [width' (* (/ width lprop) gprop)
|
(> gprop lprop)
|
||||||
padding (/ (- width' width) 2)]
|
(let [width' (* (/ width lprop) gprop)
|
||||||
(-> srect
|
padding (/ (- width' width) 2)]
|
||||||
(update :x #(- % padding))
|
(-> srect-padded
|
||||||
(assoc :width width')
|
(update :x #(- % padding))
|
||||||
(grc/update-rect :position)))
|
(assoc :width width')
|
||||||
|
(grc/update-rect :position)))
|
||||||
|
|
||||||
(< gprop lprop)
|
(< gprop lprop)
|
||||||
(let [height' (/ (* height lprop) gprop)
|
(let [height' (/ (* height lprop) gprop)
|
||||||
padding (/ (- height' height) 2)]
|
padding (/ (- height' height) 2)]
|
||||||
(-> srect
|
(-> srect-padded
|
||||||
(update :y #(- % padding))
|
(update :y #(- % padding))
|
||||||
(assoc :height height')
|
(assoc :height height')
|
||||||
(grc/update-rect :position)))
|
(grc/update-rect :position)))
|
||||||
|
|
||||||
:else
|
:else
|
||||||
(grc/update-rect srect :position)))))
|
(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)
|
||||||
|
(grc/update-rect :position)))
|
||||||
|
adjusted-rect))))
|
||||||
|
|||||||
@@ -19,10 +19,3 @@
|
|||||||
|
|
||||||
(def schema:role
|
(def schema:role
|
||||||
[::sm/one-of {:title "TeamRole"} valid-roles])
|
[::sm/one-of {:title "TeamRole"} valid-roles])
|
||||||
|
|
||||||
;; FIXME: specify more fields
|
|
||||||
(def schema:team
|
|
||||||
[:map {:title "Team"}
|
|
||||||
[:id ::sm/uuid]
|
|
||||||
[:name :string]])
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@
|
|||||||
|
|
||||||
(def token-name-ref
|
(def token-name-ref
|
||||||
[:re {:title "TokenNameRef" :gen/gen sg/text}
|
[: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
|
(def ^:private schema:color
|
||||||
[:map
|
[:map
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ desc: Create, deploy, and use the Penpot plugin API with our comprehensive docum
|
|||||||
|
|
||||||
# Penpot plugins API
|
# Penpot plugins API
|
||||||
|
|
||||||
We've got all the documentation you need for the API right <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/">here</a>.
|
We've got all the documentation you need for the API right <a target="_blank" href="https://doc.plugins.penpot.app/">here</a>.
|
||||||
|
|||||||
@@ -9,13 +9,13 @@ desc: See the Penpot plugin API changelog for version 1.0! Find breaking changes
|
|||||||
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"></g-emoji> Epics and highlights</code>
|
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f680.png"></g-emoji> Epics and highlights</code>
|
||||||
- This marks the release of version 1.0, and from this point forward, we’ll do our best to avoid making any more breaking changes (or make deprecations backward compatible).
|
- This marks the release of version 1.0, and from this point forward, we’ll do our best to avoid making any more breaking changes (or make deprecations backward compatible).
|
||||||
- We’ve redone the documentation. You can check the API here:
|
- We’ve redone the documentation. You can check the API here:
|
||||||
[https://penpot-plugins-api-doc.pages.dev/](https://penpot-plugins-api-doc.pages.dev/)
|
[https://doc.plugins.penpot.app/](https://doc.plugins.penpot.app/)
|
||||||
- New samples repository with lots of samples to use the API:
|
- New samples repository with lots of samples to use the API:
|
||||||
[https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples)
|
[https://github.com/penpot/penpot-plugins-samples](https://github.com/penpot/penpot-plugins-samples)
|
||||||
|
|
||||||
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> Breaking changes & Deprecations
|
### <g-emoji class="g-emoji" alias="boom" fallback-src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"><img class="emoji" alt="boom" height="20" width="20" src="https://github.githubassets.com/images/icons/emoji/unicode/1f4a5.png"></g-emoji> Breaking changes & Deprecations
|
||||||
|
|
||||||
- Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://penpot-plugins-api-doc.pages.dev/) for more details.
|
- Changed types names to remove the Penpot prefix. So for example: <code class="language-js">PenpotShape</code> becomes <code class="language-js">Shape</code>; <code class="language-js">PenpotFile</code> becomes <code class="language-js">File</code>, and so on. Check the [API documentation](https://doc.plugins.penpot.app/) for more details.
|
||||||
- Changes on the <code class="language-js">penpot.on</code> and <code class="language-js">penpot.off</code> methods.
|
- Changes on the <code class="language-js">penpot.on</code> and <code class="language-js">penpot.off</code> methods.
|
||||||
Previously you had to send the original callback to the off method in order to remove an event listener. Now, <code class="language-js">penpot.on</code> will return an *id* that you can pass to the <code class="language-js">penpot.off</code> method in order to remove the listener.
|
Previously you had to send the original callback to the off method in order to remove an event listener. Now, <code class="language-js">penpot.on</code> will return an *id* that you can pass to the <code class="language-js">penpot.off</code> method in order to remove the listener.
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ There are two libraries that can help you with your plugin's development. They a
|
|||||||
|
|
||||||
### Plugin styles
|
### Plugin styles
|
||||||
|
|
||||||
<code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">Plugin styles</a>.
|
<code class="language-js">@penpot/plugin-styles</code> contains styles to help build the UI for Penpot plugins. To check the styles go to <a target="_blank" href="https://styles-doc.plugins.penpot.app/">Plugin styles</a>.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install @penpot/plugin-styles
|
npm install @penpot/plugin-styles
|
||||||
@@ -139,7 +139,7 @@ parent.postMessage(responseMessage, targetOrigin);
|
|||||||
|
|
||||||
By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly.
|
By using these message-based events, any data retrieved through the Penpot API can be communicated to and from your plugin interface seamlessly.
|
||||||
|
|
||||||
For more detailed information, refer to the [Penpot Plugins API Documentation](https://penpot-plugins-api-doc.pages.dev/).
|
For more detailed information, refer to the [Penpot Plugins API Documentation](https://doc.plugins.penpot.app/).
|
||||||
|
|
||||||
## 2.5. Step 5. Build the plugin file
|
## 2.5. Step 5. Build the plugin file
|
||||||
|
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ penpot.library.local.createTypography();
|
|||||||
|
|
||||||
Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design.
|
Penpot has dark and light modes, and you can easily add this to your plugin so your interface adapts to both themes. When you add theme support, your plugin will automatically sync with Penpot's interface settings, so the user experience is consistent no matter which mode is selected. This makes your plugin look better and also ensures it stays in line with Penpot's overall design.
|
||||||
|
|
||||||
Just a heads-up: if you use the <a target="_blank" href="https://penpot-plugins-styles.pages.dev/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example.
|
Just a heads-up: if you use the <a target="_blank" href="https://styles-doc.plugins.penpot.app/">plugin-styles library</a>, many elements will automatically adapt to dark or light mode without any extra effort from you. However, if you need to customize specific elements, be sure to use the selectors provided in the <code class="language-bash">styles.css</code> of the example.
|
||||||
|
|
||||||
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/theme">Theme example</a>
|
<a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/theme">Theme example</a>
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ The plugin <a target="_blank" href="https://www.npmjs.com/package/@penpot/plugin
|
|||||||
|
|
||||||
### Is the API ready to use the prototyping features?
|
### Is the API ready to use the prototyping features?
|
||||||
|
|
||||||
Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotFlow">PenpotFlow</a> or <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/PenpotInteraction">PenpotInteraction</a> interfaces.
|
Absolutely! You can definitely create flows and interactions in the same elements as in the interface, like frames, shapes, and groups. Just check out the API documentation for the methods: createFlow, addInteraction, or removeInteraction. And if you need more help, you can always check out the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Flow">Flow</a> or <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Interaction">Interaction</a> interfaces.
|
||||||
|
|
||||||
### Are there any security or quality criteria I should be aware of?
|
### Are there any security or quality criteria I should be aware of?
|
||||||
|
|
||||||
@@ -48,7 +48,8 @@ There are no set requirements. However, we can recommend the use of <a target="_
|
|||||||
|
|
||||||
### Is it necessary to create plugins with a UI?
|
### Is it necessary to create plugins with a UI?
|
||||||
|
|
||||||
No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette-penpot-plugin.pages.dev/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot-plugins/tree/main/apps/create-palette-plugin">here</a>
|
No, it’s completely optional, in fact, we have an example of a plugin without UI. Try the plugin using this url to install it: <code class="language-js">https:\/\/create-palette.plugins.penpot.app/assets/manifest.json</code> or check the code <a target="_blank" href="https://github.com/penpot/penpot/tree/main/plugins/apps/create-palette-plugin">here</a>
|
||||||
|
|
||||||
|
|
||||||
### Can I create components?
|
### Can I create components?
|
||||||
|
|
||||||
@@ -58,7 +59,7 @@ Yes, it is possible to create components using:
|
|||||||
createComponent(shapes: Shape[]): LibraryComponent;
|
createComponent(shapes: Shape[]): LibraryComponent;
|
||||||
```
|
```
|
||||||
|
|
||||||
Take a look at the Penpot Library methods in the <a target="_blank" href="https://penpot-plugins-api-doc.pages.dev/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>.
|
Take a look at the Penpot Library methods in the <a target="_blank" href="https://doc.plugins.penpot.app/interfaces/Library">API documentation</a> or this <a target="_blank" href="https://github.com/penpot/penpot-plugins-samples/tree/main/components-library">simple example</a>.
|
||||||
|
|
||||||
### Is there a place where I can share my plugin?
|
### Is there a place where I can share my plugin?
|
||||||
|
|
||||||
|
|||||||
@@ -69,12 +69,13 @@ You need to provide the plugin's manifest URL for the installation. If there are
|
|||||||
|
|
||||||
| Name | URL |
|
| Name | URL |
|
||||||
| ------------- | ------------------------------------------------------------------- |
|
| ------------- | ------------------------------------------------------------------- |
|
||||||
| Lorem Ipsum | https://lorem-ipsum-penpot-plugin.pages.dev/assets/manifest.json |
|
| Color palette | https://create-palette.plugins.penpot.app/assets/manifest.json |
|
||||||
| Contrast | https://contrast-penpot-plugin.pages.dev/assets/manifest.json |
|
| Contrast | https://contrast.plugins.penpot.app/assets/manifest.json |
|
||||||
| Feather icons | https://icons-penpot-plugin.pages.dev/assets/manifest.json |
|
| Feather icons | https://icons.plugins.penpot.app/assets/manifest.json |
|
||||||
| Tables | https://table-penpot-plugin.pages.dev/assets/manifest.json |
|
| Lorem ipsum | https://lorem-ipsum.plugins.penpot.app/assets/manifest.json |
|
||||||
| Color palette | https://create-palette-penpot-plugin.pages.dev/assets/manifest.json |
|
| Rename layers | https://rename-layers.plugins.penpot.app/assets/manifest.json |
|
||||||
| Rename layers | https://rename-layers-penpot-plugin.pages.dev/assets/manifest.json |
|
| Tables | https://table.plugins.penpot.app/assets/manifest.json |
|
||||||
|
|
||||||
|
|
||||||
## 1.4. Plugin's basics
|
## 1.4. Plugin's basics
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/penpot/penpot"
|
"url": "https://github.com/penpot/penpot"
|
||||||
|
|||||||
@@ -12,14 +12,11 @@
|
|||||||
["node:process" :as process]
|
["node:process" :as process]
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.flags :as flags]
|
[app.common.flags :as flags]
|
||||||
[app.common.logging :as l]
|
|
||||||
[app.common.schema :as sm]
|
[app.common.schema :as sm]
|
||||||
[app.common.version :as v]
|
[app.common.version :as v]
|
||||||
[cljs.core :as c]
|
[cljs.core :as c]
|
||||||
[cuerdas.core :as str]))
|
[cuerdas.core :as str]))
|
||||||
|
|
||||||
(l/set-level! :info)
|
|
||||||
|
|
||||||
(def ^:private defaults
|
(def ^:private defaults
|
||||||
{:public-uri "http://localhost:3449"
|
{:public-uri "http://localhost:3449"
|
||||||
:tenant "default"
|
:tenant "default"
|
||||||
@@ -33,7 +30,7 @@
|
|||||||
[:map {:title "config"}
|
[:map {:title "config"}
|
||||||
[:secret-key :string]
|
[:secret-key :string]
|
||||||
[:public-uri {:optional true} ::sm/uri]
|
[:public-uri {:optional true} ::sm/uri]
|
||||||
[:exporter-shared-key {:optional true} :string]
|
[:management-api-key {:optional true} :string]
|
||||||
[:host {:optional true} :string]
|
[:host {:optional true} :string]
|
||||||
[:tenant {:optional true} :string]
|
[:tenant {:optional true} :string]
|
||||||
[:flags {:optional true} [::sm/set :keyword]]
|
[:flags {:optional true} [::sm/set :keyword]]
|
||||||
@@ -101,10 +98,8 @@
|
|||||||
(c/get config key default)))
|
(c/get config key default)))
|
||||||
|
|
||||||
(def management-key
|
(def management-key
|
||||||
(let [key (or (c/get config :exporter-shared-key)
|
(or (c/get config :management-api-key)
|
||||||
(let [secret-key (c/get config :secret-key)
|
(let [secret-key (c/get config :secret-key)
|
||||||
derived-key (crypto/hkdfSync "blake2b512" secret-key, "exporter" "" 32)]
|
derived-key (crypto/hkdfSync "blake2b512" secret-key, "management" "" 32)]
|
||||||
(-> (.from buffer/Buffer derived-key)
|
(-> (.from buffer/Buffer derived-key)
|
||||||
(.toString "base64url"))))]
|
(.toString "base64url")))))
|
||||||
(l/inf :hint "exporter key initialized" :key (d/obfuscate-string key))
|
|
||||||
key))
|
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
(p/mcat (fn [blob]
|
(p/mcat (fn [blob]
|
||||||
(let [fdata (new http/FormData)
|
(let [fdata (new http/FormData)
|
||||||
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
agent (new http/Agent #js {:connect #js {:rejectUnauthorized false}})
|
||||||
headers #js {"X-Shared-Key" (str "exporter " cf/management-key)
|
headers #js {"X-Shared-Key" cf/management-key
|
||||||
"Authorization" (str "Bearer " auth-token)}
|
"Authorization" (str "Bearer " auth-token)}
|
||||||
|
|
||||||
request #js {:headers headers
|
request #js {:headers headers
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"author": "Kaleidos INC",
|
"author": "Kaleidos INC",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6",
|
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264",
|
||||||
"browserslist": [
|
"browserslist": [
|
||||||
"defaults"
|
"defaults"
|
||||||
],
|
],
|
||||||
|
|||||||
155
frontend/playwright/data/render-wasm/get-file-flex-layouts.json
Normal file
155
frontend/playwright/data/render-wasm/get-file-flex-layouts.json
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"~: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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -58,10 +58,10 @@ export class WorkspacePage extends BaseWebSocketPage {
|
|||||||
|
|
||||||
async waitForTextSpan(nth = 0) {
|
async waitForTextSpan(nth = 0) {
|
||||||
if (!nth) {
|
if (!nth) {
|
||||||
return this.page.waitForSelector('[data-itype="inline"]');
|
return this.page.waitForSelector('[data-itype="span"]');
|
||||||
}
|
}
|
||||||
return this.page.waitForSelector(
|
return this.page.waitForSelector(
|
||||||
`[data-itype="inline"]:nth-child(${nth})`,
|
`[data-itype="span"]:nth-child(${nth})`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -210,6 +210,22 @@ test("Renders a file with shadows applied to any kind of shape", async ({
|
|||||||
await expect(workspace.canvas).toHaveScreenshot();
|
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 ({
|
test("Renders a file with a closed path shape with multiple segments using strokes and shadow", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
226
frontend/pnpm-lock.yaml
generated
226
frontend/pnpm-lock.yaml
generated
@@ -99,7 +99,7 @@ importers:
|
|||||||
version: 1.15.4
|
version: 1.15.4
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^27.4.0
|
specifier: ^27.4.0
|
||||||
version: 27.4.0
|
version: 27.4.0(canvas@3.2.1)
|
||||||
lodash:
|
lodash:
|
||||||
specifier: ^4.17.21
|
specifier: ^4.17.21
|
||||||
version: 4.17.21
|
version: 4.17.21
|
||||||
@@ -210,7 +210,7 @@ importers:
|
|||||||
version: 7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
version: 7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^4.0.18
|
specifier: ^4.0.18
|
||||||
version: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
version: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
wait-on:
|
wait-on:
|
||||||
specifier: ^9.0.3
|
specifier: ^9.0.3
|
||||||
version: 9.0.3
|
version: 9.0.3
|
||||||
@@ -265,12 +265,15 @@ importers:
|
|||||||
'@vitest/ui':
|
'@vitest/ui':
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.1(vitest@1.6.1)
|
version: 1.6.1(vitest@1.6.1)
|
||||||
|
canvas:
|
||||||
|
specifier: ^3.2.1
|
||||||
|
version: 3.2.1
|
||||||
esbuild:
|
esbuild:
|
||||||
specifier: ^0.27.2
|
specifier: ^0.27.2
|
||||||
version: 0.27.2
|
version: 0.27.2
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^27.4.0
|
specifier: ^27.4.0
|
||||||
version: 27.4.0
|
version: 27.4.0(canvas@3.2.1)
|
||||||
playwright:
|
playwright:
|
||||||
specifier: ^1.45.1
|
specifier: ^1.45.1
|
||||||
version: 1.57.0
|
version: 1.57.0
|
||||||
@@ -282,7 +285,7 @@ importers:
|
|||||||
version: 5.4.21(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)
|
version: 5.4.21(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^1.6.0
|
specifier: ^1.6.0
|
||||||
version: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)
|
version: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)
|
||||||
|
|
||||||
packages:
|
packages:
|
||||||
|
|
||||||
@@ -1751,6 +1754,9 @@ packages:
|
|||||||
bintrees@1.0.2:
|
bintrees@1.0.2:
|
||||||
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==}
|
||||||
|
|
||||||
body-parser@2.2.1:
|
body-parser@2.2.1:
|
||||||
resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==}
|
resolution: {integrity: sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
@@ -1779,6 +1785,9 @@ packages:
|
|||||||
buffer-from@1.1.2:
|
buffer-from@1.1.2:
|
||||||
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==}
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
|
||||||
|
|
||||||
@@ -1809,6 +1818,10 @@ packages:
|
|||||||
caniuse-lite@1.0.30001762:
|
caniuse-lite@1.0.30001762:
|
||||||
resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==}
|
resolution: {integrity: sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==}
|
||||||
|
|
||||||
|
canvas@3.2.1:
|
||||||
|
resolution: {integrity: sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==}
|
||||||
|
engines: {node: ^18.12.0 || >= 20.9.0}
|
||||||
|
|
||||||
chai@4.5.0:
|
chai@4.5.0:
|
||||||
resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
|
resolution: {integrity: sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -1851,6 +1864,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==}
|
||||||
engines: {node: '>= 14.16.0'}
|
engines: {node: '>= 14.16.0'}
|
||||||
|
|
||||||
|
chownr@1.1.4:
|
||||||
|
resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
|
||||||
|
|
||||||
chownr@2.0.0:
|
chownr@2.0.0:
|
||||||
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -2092,6 +2108,10 @@ packages:
|
|||||||
decimal.js@10.6.0:
|
decimal.js@10.6.0:
|
||||||
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==}
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
deep-eql@4.1.4:
|
deep-eql@4.1.4:
|
||||||
resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
|
resolution: {integrity: sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
@@ -2100,6 +2120,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==}
|
||||||
engines: {node: '>=6'}
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
|
deep-extend@0.6.0:
|
||||||
|
resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==}
|
||||||
|
engines: {node: '>=4.0.0'}
|
||||||
|
|
||||||
deepmerge@4.3.1:
|
deepmerge@4.3.1:
|
||||||
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
resolution: {integrity: sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
@@ -2144,6 +2168,10 @@ packages:
|
|||||||
engines: {node: '>=0.10'}
|
engines: {node: '>=0.10'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
detect-libc@2.1.2:
|
||||||
|
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||||
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
dettle@1.0.5:
|
dettle@1.0.5:
|
||||||
resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==}
|
resolution: {integrity: sha512-ZVyjhAJ7sCe1PNXEGveObOH9AC8QvMga3HJIghHawtG7mE4K5pW9nz/vDGAr/U7a3LWgdOzEE7ac9MURnyfaTA==}
|
||||||
|
|
||||||
@@ -2232,6 +2260,9 @@ packages:
|
|||||||
encoding@0.1.13:
|
encoding@0.1.13:
|
||||||
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
resolution: {integrity: sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==}
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==}
|
||||||
|
|
||||||
entities@2.2.0:
|
entities@2.2.0:
|
||||||
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==}
|
||||||
|
|
||||||
@@ -2329,6 +2360,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
|
||||||
engines: {node: '>=16.17'}
|
engines: {node: '>=16.17'}
|
||||||
|
|
||||||
|
expand-template@2.0.3:
|
||||||
|
resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
expect-type@1.3.0:
|
expect-type@1.3.0:
|
||||||
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
|
||||||
engines: {node: '>=12.0.0'}
|
engines: {node: '>=12.0.0'}
|
||||||
@@ -2421,6 +2456,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
|
fs-constants@1.0.0:
|
||||||
|
resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==}
|
||||||
|
|
||||||
fs-extra@10.1.0:
|
fs-extra@10.1.0:
|
||||||
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
@@ -2493,6 +2531,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-eFmhDi2xQ+2reMRY2AbJ2oa10uFOl1oyGbAKdCZiNOk94NJHi7aN0OBELSC9v35ZAPQdr+uRBi93/Gu4SlBdrA==}
|
resolution: {integrity: sha512-eFmhDi2xQ+2reMRY2AbJ2oa10uFOl1oyGbAKdCZiNOk94NJHi7aN0OBELSC9v35ZAPQdr+uRBi93/Gu4SlBdrA==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
||||||
|
github-from-package@0.0.0:
|
||||||
|
resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
|
||||||
engines: {node: '>= 6'}
|
engines: {node: '>= 6'}
|
||||||
@@ -3042,6 +3083,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
resolution: {integrity: sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
mimic-response@3.1.0:
|
||||||
|
resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
min-indent@1.0.1:
|
min-indent@1.0.1:
|
||||||
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -3080,6 +3125,9 @@ packages:
|
|||||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3:
|
||||||
|
resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==}
|
||||||
|
|
||||||
mkdirp@1.0.4:
|
mkdirp@1.0.4:
|
||||||
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -3112,6 +3160,9 @@ packages:
|
|||||||
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0:
|
||||||
|
resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==}
|
||||||
|
|
||||||
negotiator@0.6.4:
|
negotiator@0.6.4:
|
||||||
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
resolution: {integrity: sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
@@ -3123,6 +3174,10 @@ packages:
|
|||||||
nice-try@1.0.5:
|
nice-try@1.0.5:
|
||||||
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
resolution: {integrity: sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==}
|
||||||
|
|
||||||
|
node-abi@3.87.0:
|
||||||
|
resolution: {integrity: sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-addon-api@7.1.1:
|
||||||
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
|
||||||
|
|
||||||
@@ -3415,6 +3470,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
|
||||||
engines: {node: ^10 || ^12 || >=14}
|
engines: {node: ^10 || ^12 || >=14}
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
prettier@3.5.3:
|
prettier@3.5.3:
|
||||||
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
resolution: {integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
@@ -3472,6 +3532,9 @@ packages:
|
|||||||
pstree.remy@1.1.8:
|
pstree.remy@1.1.8:
|
||||||
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
resolution: {integrity: sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==}
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
resolution: {integrity: sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==}
|
||||||
|
|
||||||
punycode@1.4.1:
|
punycode@1.4.1:
|
||||||
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==}
|
||||||
|
|
||||||
@@ -3497,6 +3560,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
resolution: {integrity: sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==}
|
||||||
engines: {node: '>= 0.10'}
|
engines: {node: '>= 0.10'}
|
||||||
|
|
||||||
|
rc@1.2.8:
|
||||||
|
resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
react-docgen-typescript@2.4.0:
|
react-docgen-typescript@2.4.0:
|
||||||
resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==}
|
resolution: {integrity: sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -3884,6 +3951,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==}
|
||||||
engines: {node: '>=14'}
|
engines: {node: '>=14'}
|
||||||
|
|
||||||
|
simple-concat@1.0.1:
|
||||||
|
resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==}
|
||||||
|
|
||||||
simple-update-notifier@2.0.0:
|
simple-update-notifier@2.0.0:
|
||||||
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4016,6 +4089,10 @@ packages:
|
|||||||
resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
|
resolution: {integrity: sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==}
|
||||||
engines: {node: '>=12'}
|
engines: {node: '>=12'}
|
||||||
|
|
||||||
|
strip-json-comments@2.0.1:
|
||||||
|
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||||
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
strip-literal@2.1.1:
|
strip-literal@2.1.1:
|
||||||
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
resolution: {integrity: sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==}
|
||||||
|
|
||||||
@@ -4069,6 +4146,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
|
resolution: {integrity: sha512-GTt8rSKje5FilG+wEdfCkOcLL7LWqpMlr2c3LRuKt/YXxcJ52aGSbGBAdI4L3aaqfrBt6y711El53ItyH1NWzg==}
|
||||||
engines: {node: '>=16.0.0'}
|
engines: {node: '>=16.0.0'}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==}
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
|
||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@@ -4195,6 +4279,9 @@ packages:
|
|||||||
tslib@2.8.1:
|
tslib@2.8.1:
|
||||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||||
|
|
||||||
|
tunnel-agent@0.6.0:
|
||||||
|
resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==}
|
||||||
|
|
||||||
type-detect@4.1.0:
|
type-detect@4.1.0:
|
||||||
resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
|
resolution: {integrity: sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==}
|
||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
@@ -5411,7 +5498,7 @@ snapshots:
|
|||||||
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
||||||
'@vitest/browser-playwright': 4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
'@vitest/browser-playwright': 4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
||||||
'@vitest/runner': 4.0.18
|
'@vitest/runner': 4.0.18
|
||||||
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- react
|
- react
|
||||||
- react-dom
|
- react-dom
|
||||||
@@ -5583,7 +5670,7 @@ snapshots:
|
|||||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))
|
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))
|
||||||
playwright: 1.58.0
|
playwright: 1.58.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
- msw
|
- msw
|
||||||
@@ -5595,7 +5682,7 @@ snapshots:
|
|||||||
'@vitest/utils': 1.6.1
|
'@vitest/utils': 1.6.1
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
sirv: 2.0.4
|
sirv: 2.0.4
|
||||||
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)
|
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
playwright: 1.57.0
|
playwright: 1.57.0
|
||||||
|
|
||||||
@@ -5608,7 +5695,7 @@ snapshots:
|
|||||||
pngjs: 7.0.0
|
pngjs: 7.0.0
|
||||||
sirv: 3.0.2
|
sirv: 3.0.2
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
ws: 8.19.0
|
ws: 8.19.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -5631,7 +5718,7 @@ snapshots:
|
|||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
strip-literal: 2.1.1
|
strip-literal: 2.1.1
|
||||||
test-exclude: 6.0.0
|
test-exclude: 6.0.0
|
||||||
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)
|
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -5647,7 +5734,7 @@ snapshots:
|
|||||||
obug: 2.1.1
|
obug: 2.1.1
|
||||||
std-env: 3.10.0
|
std-env: 3.10.0
|
||||||
tinyrainbow: 3.0.3
|
tinyrainbow: 3.0.3
|
||||||
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
vitest: 4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
'@vitest/browser': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
||||||
|
|
||||||
@@ -5740,7 +5827,7 @@ snapshots:
|
|||||||
pathe: 1.1.2
|
pathe: 1.1.2
|
||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
sirv: 2.0.4
|
sirv: 2.0.4
|
||||||
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)
|
vitest: 1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)
|
||||||
|
|
||||||
'@vitest/utils@1.6.1':
|
'@vitest/utils@1.6.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -5908,6 +5995,12 @@ snapshots:
|
|||||||
|
|
||||||
bintrees@1.0.2: {}
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
|
bl@4.1.0:
|
||||||
|
dependencies:
|
||||||
|
buffer: 5.7.1
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
body-parser@2.2.1:
|
body-parser@2.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
bytes: 3.1.2
|
bytes: 3.1.2
|
||||||
@@ -5949,6 +6042,11 @@ snapshots:
|
|||||||
|
|
||||||
buffer-from@1.1.2: {}
|
buffer-from@1.1.2: {}
|
||||||
|
|
||||||
|
buffer@5.7.1:
|
||||||
|
dependencies:
|
||||||
|
base64-js: 1.5.1
|
||||||
|
ieee754: 1.2.1
|
||||||
|
|
||||||
buffer@6.0.3:
|
buffer@6.0.3:
|
||||||
dependencies:
|
dependencies:
|
||||||
base64-js: 1.5.1
|
base64-js: 1.5.1
|
||||||
@@ -5981,6 +6079,11 @@ snapshots:
|
|||||||
|
|
||||||
caniuse-lite@1.0.30001762: {}
|
caniuse-lite@1.0.30001762: {}
|
||||||
|
|
||||||
|
canvas@3.2.1:
|
||||||
|
dependencies:
|
||||||
|
node-addon-api: 7.1.1
|
||||||
|
prebuild-install: 7.1.3
|
||||||
|
|
||||||
chai@4.5.0:
|
chai@4.5.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
assertion-error: 1.1.0
|
assertion-error: 1.1.0
|
||||||
@@ -6038,6 +6141,8 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
readdirp: 4.1.2
|
readdirp: 4.1.2
|
||||||
|
|
||||||
|
chownr@1.1.4: {}
|
||||||
|
|
||||||
chownr@2.0.0: {}
|
chownr@2.0.0: {}
|
||||||
|
|
||||||
ci-info@3.9.0: {}
|
ci-info@3.9.0: {}
|
||||||
@@ -6273,12 +6378,18 @@ snapshots:
|
|||||||
|
|
||||||
decimal.js@10.6.0: {}
|
decimal.js@10.6.0: {}
|
||||||
|
|
||||||
|
decompress-response@6.0.0:
|
||||||
|
dependencies:
|
||||||
|
mimic-response: 3.1.0
|
||||||
|
|
||||||
deep-eql@4.1.4:
|
deep-eql@4.1.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
type-detect: 4.1.0
|
type-detect: 4.1.0
|
||||||
|
|
||||||
deep-eql@5.0.2: {}
|
deep-eql@5.0.2: {}
|
||||||
|
|
||||||
|
deep-extend@0.6.0: {}
|
||||||
|
|
||||||
deepmerge@4.3.1: {}
|
deepmerge@4.3.1: {}
|
||||||
|
|
||||||
default-browser-id@5.0.1: {}
|
default-browser-id@5.0.1: {}
|
||||||
@@ -6313,6 +6424,8 @@ snapshots:
|
|||||||
detect-libc@1.0.3:
|
detect-libc@1.0.3:
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
detect-libc@2.1.2: {}
|
||||||
|
|
||||||
dettle@1.0.5: {}
|
dettle@1.0.5: {}
|
||||||
|
|
||||||
diff-sequences@29.6.3: {}
|
diff-sequences@29.6.3: {}
|
||||||
@@ -6407,6 +6520,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
iconv-lite: 0.6.3
|
iconv-lite: 0.6.3
|
||||||
|
|
||||||
|
end-of-stream@1.4.5:
|
||||||
|
dependencies:
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
entities@2.2.0: {}
|
entities@2.2.0: {}
|
||||||
|
|
||||||
entities@4.5.0: {}
|
entities@4.5.0: {}
|
||||||
@@ -6588,6 +6705,8 @@ snapshots:
|
|||||||
signal-exit: 4.1.0
|
signal-exit: 4.1.0
|
||||||
strip-final-newline: 3.0.0
|
strip-final-newline: 3.0.0
|
||||||
|
|
||||||
|
expand-template@2.0.3: {}
|
||||||
|
|
||||||
expect-type@1.3.0: {}
|
expect-type@1.3.0: {}
|
||||||
|
|
||||||
expr-eval-fork@2.0.2: {}
|
expr-eval-fork@2.0.2: {}
|
||||||
@@ -6711,6 +6830,8 @@ snapshots:
|
|||||||
|
|
||||||
fresh@2.0.0: {}
|
fresh@2.0.0: {}
|
||||||
|
|
||||||
|
fs-constants@1.0.0: {}
|
||||||
|
|
||||||
fs-extra@10.1.0:
|
fs-extra@10.1.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
graceful-fs: 4.2.11
|
graceful-fs: 4.2.11
|
||||||
@@ -6789,6 +6910,8 @@ snapshots:
|
|||||||
readable-stream: 4.7.0
|
readable-stream: 4.7.0
|
||||||
safe-buffer: 5.2.1
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
|
github-from-package@0.0.0: {}
|
||||||
|
|
||||||
glob-parent@5.1.2:
|
glob-parent@5.1.2:
|
||||||
dependencies:
|
dependencies:
|
||||||
is-glob: 4.0.3
|
is-glob: 4.0.3
|
||||||
@@ -7163,7 +7286,7 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
argparse: 2.0.1
|
argparse: 2.0.1
|
||||||
|
|
||||||
jsdom@27.4.0:
|
jsdom@27.4.0(canvas@3.2.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@acemir/cssom': 0.9.30
|
'@acemir/cssom': 0.9.30
|
||||||
'@asamuzakjp/dom-selector': 6.7.6
|
'@asamuzakjp/dom-selector': 6.7.6
|
||||||
@@ -7185,6 +7308,8 @@ snapshots:
|
|||||||
whatwg-url: 15.1.0
|
whatwg-url: 15.1.0
|
||||||
ws: 8.18.3
|
ws: 8.18.3
|
||||||
xml-name-validator: 5.0.0
|
xml-name-validator: 5.0.0
|
||||||
|
optionalDependencies:
|
||||||
|
canvas: 3.2.1
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@exodus/crypto'
|
- '@exodus/crypto'
|
||||||
- bufferutil
|
- bufferutil
|
||||||
@@ -7342,6 +7467,8 @@ snapshots:
|
|||||||
|
|
||||||
mimic-fn@4.0.0: {}
|
mimic-fn@4.0.0: {}
|
||||||
|
|
||||||
|
mimic-response@3.1.0: {}
|
||||||
|
|
||||||
min-indent@1.0.1: {}
|
min-indent@1.0.1: {}
|
||||||
|
|
||||||
minimatch@10.1.1:
|
minimatch@10.1.1:
|
||||||
@@ -7375,6 +7502,8 @@ snapshots:
|
|||||||
minipass: 3.3.6
|
minipass: 3.3.6
|
||||||
yallist: 4.0.0
|
yallist: 4.0.0
|
||||||
|
|
||||||
|
mkdirp-classic@0.5.3: {}
|
||||||
|
|
||||||
mkdirp@1.0.4: {}
|
mkdirp@1.0.4: {}
|
||||||
|
|
||||||
mkdirp@3.0.1: {}
|
mkdirp@3.0.1: {}
|
||||||
@@ -7396,14 +7525,19 @@ snapshots:
|
|||||||
|
|
||||||
nanoid@3.3.11: {}
|
nanoid@3.3.11: {}
|
||||||
|
|
||||||
|
napi-build-utils@2.0.0: {}
|
||||||
|
|
||||||
negotiator@0.6.4: {}
|
negotiator@0.6.4: {}
|
||||||
|
|
||||||
negotiator@1.0.0: {}
|
negotiator@1.0.0: {}
|
||||||
|
|
||||||
nice-try@1.0.5: {}
|
nice-try@1.0.5: {}
|
||||||
|
|
||||||
node-addon-api@7.1.1:
|
node-abi@3.87.0:
|
||||||
optional: true
|
dependencies:
|
||||||
|
semver: 7.7.3
|
||||||
|
|
||||||
|
node-addon-api@7.1.1: {}
|
||||||
|
|
||||||
node-fetch@2.7.0(encoding@0.1.13):
|
node-fetch@2.7.0(encoding@0.1.13):
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -7704,6 +7838,21 @@ snapshots:
|
|||||||
picocolors: 1.1.1
|
picocolors: 1.1.1
|
||||||
source-map-js: 1.2.1
|
source-map-js: 1.2.1
|
||||||
|
|
||||||
|
prebuild-install@7.1.3:
|
||||||
|
dependencies:
|
||||||
|
detect-libc: 2.1.2
|
||||||
|
expand-template: 2.0.3
|
||||||
|
github-from-package: 0.0.0
|
||||||
|
minimist: 1.2.8
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
napi-build-utils: 2.0.0
|
||||||
|
node-abi: 3.87.0
|
||||||
|
pump: 3.0.3
|
||||||
|
rc: 1.2.8
|
||||||
|
simple-get: 4.0.1
|
||||||
|
tar-fs: 2.1.4
|
||||||
|
tunnel-agent: 0.6.0
|
||||||
|
|
||||||
prettier@3.5.3: {}
|
prettier@3.5.3: {}
|
||||||
|
|
||||||
prettier@3.7.4: {}
|
prettier@3.7.4: {}
|
||||||
@@ -7755,6 +7904,11 @@ snapshots:
|
|||||||
|
|
||||||
pstree.remy@1.1.8: {}
|
pstree.remy@1.1.8: {}
|
||||||
|
|
||||||
|
pump@3.0.3:
|
||||||
|
dependencies:
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
once: 1.4.0
|
||||||
|
|
||||||
punycode@1.4.1: {}
|
punycode@1.4.1: {}
|
||||||
|
|
||||||
punycode@2.3.1: {}
|
punycode@2.3.1: {}
|
||||||
@@ -7776,6 +7930,13 @@ snapshots:
|
|||||||
iconv-lite: 0.7.1
|
iconv-lite: 0.7.1
|
||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
|
|
||||||
|
rc@1.2.8:
|
||||||
|
dependencies:
|
||||||
|
deep-extend: 0.6.0
|
||||||
|
ini: 1.3.8
|
||||||
|
minimist: 1.2.8
|
||||||
|
strip-json-comments: 2.0.1
|
||||||
|
|
||||||
react-docgen-typescript@2.4.0(typescript@5.9.3):
|
react-docgen-typescript@2.4.0(typescript@5.9.3):
|
||||||
dependencies:
|
dependencies:
|
||||||
typescript: 5.9.3
|
typescript: 5.9.3
|
||||||
@@ -8249,6 +8410,14 @@ snapshots:
|
|||||||
|
|
||||||
signal-exit@4.1.0: {}
|
signal-exit@4.1.0: {}
|
||||||
|
|
||||||
|
simple-concat@1.0.1: {}
|
||||||
|
|
||||||
|
simple-get@4.0.1:
|
||||||
|
dependencies:
|
||||||
|
decompress-response: 6.0.0
|
||||||
|
once: 1.4.0
|
||||||
|
simple-concat: 1.0.1
|
||||||
|
|
||||||
simple-update-notifier@2.0.0:
|
simple-update-notifier@2.0.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
semver: 7.7.3
|
semver: 7.7.3
|
||||||
@@ -8404,6 +8573,8 @@ snapshots:
|
|||||||
|
|
||||||
strip-indent@4.1.1: {}
|
strip-indent@4.1.1: {}
|
||||||
|
|
||||||
|
strip-json-comments@2.0.1: {}
|
||||||
|
|
||||||
strip-literal@2.1.1:
|
strip-literal@2.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
js-tokens: 9.0.1
|
js-tokens: 9.0.1
|
||||||
@@ -8487,6 +8658,21 @@ snapshots:
|
|||||||
|
|
||||||
sync-message-port@1.1.3: {}
|
sync-message-port@1.1.3: {}
|
||||||
|
|
||||||
|
tar-fs@2.1.4:
|
||||||
|
dependencies:
|
||||||
|
chownr: 1.1.4
|
||||||
|
mkdirp-classic: 0.5.3
|
||||||
|
pump: 3.0.3
|
||||||
|
tar-stream: 2.2.0
|
||||||
|
|
||||||
|
tar-stream@2.2.0:
|
||||||
|
dependencies:
|
||||||
|
bl: 4.1.0
|
||||||
|
end-of-stream: 1.4.5
|
||||||
|
fs-constants: 1.0.0
|
||||||
|
inherits: 2.0.4
|
||||||
|
readable-stream: 3.6.2
|
||||||
|
|
||||||
tar@6.2.1:
|
tar@6.2.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
chownr: 2.0.0
|
chownr: 2.0.0
|
||||||
@@ -8587,6 +8773,10 @@ snapshots:
|
|||||||
|
|
||||||
tslib@2.8.1: {}
|
tslib@2.8.1: {}
|
||||||
|
|
||||||
|
tunnel-agent@0.6.0:
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: 5.2.1
|
||||||
|
|
||||||
type-detect@4.1.0: {}
|
type-detect@4.1.0: {}
|
||||||
|
|
||||||
type-is@2.0.1:
|
type-is@2.0.1:
|
||||||
@@ -8757,7 +8947,7 @@ snapshots:
|
|||||||
sass-embedded: 1.97.1
|
sass-embedded: 1.97.1
|
||||||
yaml: 2.8.2
|
yaml: 2.8.2
|
||||||
|
|
||||||
vitest@1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1):
|
vitest@1.6.1(@types/node@25.0.3)(@vitest/browser@1.6.1)(@vitest/ui@1.6.1)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 1.6.1
|
'@vitest/expect': 1.6.1
|
||||||
'@vitest/runner': 1.6.1
|
'@vitest/runner': 1.6.1
|
||||||
@@ -8783,7 +8973,7 @@ snapshots:
|
|||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
'@vitest/browser': 1.6.1(playwright@1.57.0)(vitest@1.6.1)
|
'@vitest/browser': 1.6.1(playwright@1.57.0)(vitest@1.6.1)
|
||||||
'@vitest/ui': 1.6.1(vitest@1.6.1)
|
'@vitest/ui': 1.6.1(vitest@1.6.1)
|
||||||
jsdom: 27.4.0
|
jsdom: 27.4.0(canvas@3.2.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- less
|
- less
|
||||||
- lightningcss
|
- lightningcss
|
||||||
@@ -8794,7 +8984,7 @@ snapshots:
|
|||||||
- supports-color
|
- supports-color
|
||||||
- terser
|
- terser
|
||||||
|
|
||||||
vitest@4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2):
|
vitest@4.0.18(@types/node@25.0.3)(@vitest/browser-playwright@4.0.18)(jsdom@27.4.0(canvas@3.2.1))(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.0.18
|
'@vitest/expect': 4.0.18
|
||||||
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))
|
'@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))
|
||||||
@@ -8819,7 +9009,7 @@ snapshots:
|
|||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 25.0.3
|
'@types/node': 25.0.3
|
||||||
'@vitest/browser-playwright': 4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
'@vitest/browser-playwright': 4.0.18(playwright@1.58.0)(vite@7.3.1(@types/node@25.0.3)(sass-embedded@1.97.1)(sass@1.97.1)(yaml@2.8.2))(vitest@4.0.18)
|
||||||
jsdom: 27.4.0
|
jsdom: 27.4.0(canvas@3.2.1)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
@@ -214,8 +214,8 @@
|
|||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [change-fn
|
(let [change-fn
|
||||||
(fn [shape attrs]
|
(fn [node attrs]
|
||||||
(update shape :fills types.fills/prepend attrs))
|
(update node :fills types.fills/prepend attrs))
|
||||||
undo-id
|
undo-id
|
||||||
(js/Symbol)]
|
(js/Symbol)]
|
||||||
(rx/concat
|
(rx/concat
|
||||||
|
|||||||
@@ -51,7 +51,7 @@
|
|||||||
|
|
||||||
(or (> (:width srect) width)
|
(or (> (:width srect) width)
|
||||||
(> (:height srect) height))
|
(> (:height srect) height))
|
||||||
(let [srect (gal/adjust-to-viewport size srect {:padding 40})
|
(let [srect (gal/adjust-to-viewport size srect {:padding 40 :min-zoom 0.01})
|
||||||
zoom (/ (:width size) (:width srect))]
|
zoom (/ (:width size) (:width srect))]
|
||||||
|
|
||||||
(-> local
|
(-> local
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
state
|
state
|
||||||
(update state :workspace-local
|
(update state :workspace-local
|
||||||
(fn [{:keys [vport] :as local}]
|
(fn [{:keys [vport] :as local}]
|
||||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 160})
|
(let [srect (gal/adjust-to-viewport vport srect {:padding 160 :min-zoom 0.01})
|
||||||
zoom (/ (:width vport) (:width srect))]
|
zoom (/ (:width vport) (:width srect))]
|
||||||
(-> local
|
(-> local
|
||||||
(assoc :zoom zoom)
|
(assoc :zoom zoom)
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
(gsh/shapes->rect))]
|
(gsh/shapes->rect))]
|
||||||
(update state :workspace-local
|
(update state :workspace-local
|
||||||
(fn [{:keys [vport] :as local}]
|
(fn [{:keys [vport] :as local}]
|
||||||
(let [srect (gal/adjust-to-viewport vport srect {:padding 40})
|
(let [srect (gal/adjust-to-viewport vport srect {:padding 40 :min-zoom 0.01})
|
||||||
zoom (/ (:width vport) (:width srect))]
|
zoom (/ (:width vport) (:width srect))]
|
||||||
(-> local
|
(-> local
|
||||||
(assoc :zoom zoom)
|
(assoc :zoom zoom)
|
||||||
@@ -142,7 +142,7 @@
|
|||||||
(fn [{:keys [vport] :as local}]
|
(fn [{:keys [vport] :as local}]
|
||||||
(let [srect (gal/adjust-to-viewport
|
(let [srect (gal/adjust-to-viewport
|
||||||
vport srect
|
vport srect
|
||||||
{:padding 40})
|
{:padding 40 :min-zoom 0.01})
|
||||||
zoom (/ (:width vport)
|
zoom (/ (:width vport)
|
||||||
(:width srect))]
|
(:width srect))]
|
||||||
(-> local
|
(-> local
|
||||||
|
|||||||
@@ -437,7 +437,8 @@
|
|||||||
[:> menu-entry* {:title (tr "workspace.shape.menu.flatten")
|
[:> menu-entry* {:title (tr "workspace.shape.menu.flatten")
|
||||||
:on-click do-transform-to-path}])
|
:on-click do-transform-to-path}])
|
||||||
|
|
||||||
(when (and (not disable-booleans)
|
(when (and (not has-frame?)
|
||||||
|
(not disable-booleans)
|
||||||
(or multiple? (and single? (or is-group? is-bool?))))
|
(or multiple? (and single? (or is-group? is-bool?))))
|
||||||
[:> menu-entry* {:title (tr "workspace.shape.menu.path")}
|
[:> menu-entry* {:title (tr "workspace.shape.menu.path")}
|
||||||
[:> menu-entry* {:title (tr "workspace.shape.menu.union")
|
[:> menu-entry* {:title (tr "workspace.shape.menu.union")
|
||||||
|
|||||||
@@ -346,17 +346,19 @@
|
|||||||
{:value (:id variant)
|
{:value (:id variant)
|
||||||
:key (pr-str variant)
|
:key (pr-str variant)
|
||||||
:label (:name variant)})))
|
:label (:name variant)})))
|
||||||
variant-options (if (= font-variant-id :multiple)
|
variant-options (if (or (= font-variant-id :multiple) (= font-variant-id "mixed"))
|
||||||
(conj basic-variant-options
|
(conj basic-variant-options
|
||||||
{:value ""
|
{:value ""
|
||||||
:key :multiple-variants
|
:key :multiple-variants
|
||||||
:label "--"})
|
:label "--"})
|
||||||
basic-variant-options)]
|
basic-variant-options)
|
||||||
|
font-variant-value (attr->string font-variant-id)
|
||||||
|
font-variant-value (if (= font-variant-value "mixed") "" font-variant-value)]
|
||||||
|
|
||||||
;; TODO Add disabled mode
|
;; TODO Add disabled mode
|
||||||
[:& select
|
[:& select
|
||||||
{:class (stl/css :font-variant-select)
|
{:class (stl/css :font-variant-select)
|
||||||
:default-value (attr->string font-variant-id)
|
:default-value font-variant-value
|
||||||
:options variant-options
|
:options variant-options
|
||||||
:on-change on-font-variant-change
|
:on-change on-font-variant-change
|
||||||
:on-blur on-blur}])]]]))
|
:on-blur on-blur}])]]]))
|
||||||
|
|||||||
@@ -23,15 +23,15 @@
|
|||||||
[node]
|
[node]
|
||||||
(is-element node "br"))
|
(is-element node "br"))
|
||||||
|
|
||||||
(defn is-inline-child
|
(defn is-text-span-child
|
||||||
[node]
|
[node]
|
||||||
(or (is-line-break node)
|
(or (is-line-break node)
|
||||||
(is-text-node node)))
|
(is-text-node node)))
|
||||||
|
|
||||||
(defn get-inline-text
|
(defn get-text-span-text
|
||||||
[element]
|
[element]
|
||||||
(when-not (is-inline-child (.-firstChild element))
|
(when-not (is-text-span-child (.-firstChild element))
|
||||||
(throw (js/TypeError. "Invalid inline child")))
|
(throw (js/TypeError. "Invalid text span child")))
|
||||||
(if (is-line-break (.-firstChild element))
|
(if (is-line-break (.-firstChild element))
|
||||||
""
|
""
|
||||||
(.-textContent element)))
|
(.-textContent element)))
|
||||||
@@ -54,7 +54,7 @@
|
|||||||
(assoc acc key (if (value-empty? value) (get defaults key) value))))
|
(assoc acc key (if (value-empty? value) (get defaults key) value))))
|
||||||
{} attrs)))
|
{} attrs)))
|
||||||
|
|
||||||
(defn get-inline-styles
|
(defn get-text-span-styles
|
||||||
[element]
|
[element]
|
||||||
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
(get-attrs-from-styles element txt/text-node-attrs (txt/get-default-text-attrs)))
|
||||||
|
|
||||||
@@ -66,18 +66,18 @@
|
|||||||
[element]
|
[element]
|
||||||
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
(get-attrs-from-styles element txt/root-attrs txt/default-root-attrs))
|
||||||
|
|
||||||
(defn create-inline
|
(defn create-text-span
|
||||||
[element]
|
[element]
|
||||||
(let [text (get-inline-text element)]
|
(let [text (get-text-span-text element)]
|
||||||
(d/merge {:text text
|
(d/merge {:text text
|
||||||
:key (.-id element)}
|
:key (.-id element)}
|
||||||
(get-inline-styles element))))
|
(get-text-span-styles element))))
|
||||||
|
|
||||||
(defn create-paragraph
|
(defn create-paragraph
|
||||||
[element]
|
[element]
|
||||||
(d/merge {:type "paragraph"
|
(d/merge {:type "paragraph"
|
||||||
:key (.-id element)
|
:key (.-id element)
|
||||||
:children (mapv create-inline (.-children element))}
|
:children (mapv create-text-span (.-children element))}
|
||||||
(get-paragraph-styles element)))
|
(get-paragraph-styles element)))
|
||||||
|
|
||||||
(defn create-root
|
(defn create-root
|
||||||
|
|||||||
@@ -92,7 +92,7 @@
|
|||||||
[root]
|
[root]
|
||||||
(get-styles-from-attrs root txt/root-attrs txt/default-text-attrs))
|
(get-styles-from-attrs root txt/root-attrs txt/default-text-attrs))
|
||||||
|
|
||||||
(defn get-inline-styles
|
(defn get-text-span-styles
|
||||||
[inline paragraph]
|
[inline paragraph]
|
||||||
(let [node (if (= "" (:text inline)) paragraph inline)
|
(let [node (if (= "" (:text inline)) paragraph inline)
|
||||||
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
styles (get-styles-from-attrs node txt/text-node-attrs txt/default-text-attrs)]
|
||||||
@@ -104,7 +104,7 @@
|
|||||||
(when text
|
(when text
|
||||||
(.replace text (js/RegExp "/" "g") "/\u200B")))
|
(.replace text (js/RegExp "/" "g") "/\u200B")))
|
||||||
|
|
||||||
(defn get-inline-children
|
(defn get-text-span-children
|
||||||
[inline paragraph]
|
[inline paragraph]
|
||||||
[(if (and (= "" (:text inline))
|
[(if (and (= "" (:text inline))
|
||||||
(= 1 (count (:children paragraph))))
|
(= 1 (count (:children paragraph))))
|
||||||
@@ -119,14 +119,14 @@
|
|||||||
[paragraph]
|
[paragraph]
|
||||||
(some #(not= "" (:text % "")) (:children paragraph)))
|
(some #(not= "" (:text % "")) (:children paragraph)))
|
||||||
|
|
||||||
(defn create-inline
|
(defn create-text-span
|
||||||
[inline paragraph]
|
[inline paragraph]
|
||||||
(create-element
|
(create-element
|
||||||
"span"
|
"span"
|
||||||
{:id (or (:key inline) (create-random-key))
|
{:id (or (:key inline) (create-random-key))
|
||||||
:data {:itype "inline"}
|
:data {:itype "span"}
|
||||||
:style (get-inline-styles inline paragraph)}
|
:style (get-text-span-styles inline paragraph)}
|
||||||
(get-inline-children inline paragraph)))
|
(get-text-span-children inline paragraph)))
|
||||||
|
|
||||||
(defn create-paragraph
|
(defn create-paragraph
|
||||||
[paragraph]
|
[paragraph]
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
{:id (or (:key paragraph) (create-random-key))
|
{:id (or (:key paragraph) (create-random-key))
|
||||||
:data {:itype "paragraph"}
|
:data {:itype "paragraph"}
|
||||||
:style (get-paragraph-styles paragraph)}
|
:style (get-paragraph-styles paragraph)}
|
||||||
(mapv #(create-inline % paragraph) (:children paragraph))))
|
(mapv #(create-text-span % paragraph) (:children paragraph))))
|
||||||
|
|
||||||
(defn create-root
|
(defn create-root
|
||||||
[root]
|
[root]
|
||||||
|
|||||||
@@ -22,10 +22,11 @@
|
|||||||
"@vitest/ui": "^1.6.0",
|
"@vitest/ui": "^1.6.0",
|
||||||
"esbuild": "^0.27.2",
|
"esbuild": "^0.27.2",
|
||||||
"jsdom": "^27.4.0",
|
"jsdom": "^27.4.0",
|
||||||
|
"canvas": "^3.2.1",
|
||||||
"playwright": "^1.45.1",
|
"playwright": "^1.45.1",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"vite": "^5.3.1",
|
"vite": "^5.3.1",
|
||||||
"vitest": "^1.6.0"
|
"vitest": "^1.6.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@10.26.2+sha512.0e308ff2005fc7410366f154f625f6631ab2b16b1d2e70238444dd6ae9d630a8482d92a451144debc492416896ed16f7b114a86ec68b8404b2443869e68ffda6"
|
"packageManager": "pnpm@10.28.2+sha512.41872f037ad22f7348e3b1debbaf7e867cfd448f2726d9cf74c08f19507c31d2c8e7a11525b983febc2df640b5438dee6023ebb1f84ed43cc2d654d2bc326264"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,9 +130,9 @@ export class TextEditor extends EventTarget {
|
|||||||
cut: this.#onCut,
|
cut: this.#onCut,
|
||||||
copy: this.#onCopy,
|
copy: this.#onCopy,
|
||||||
|
|
||||||
|
keydown: this.#onKeyDown,
|
||||||
beforeinput: this.#onBeforeInput,
|
beforeinput: this.#onBeforeInput,
|
||||||
input: this.#onInput,
|
input: this.#onInput,
|
||||||
keydown: this.#onKeyDown,
|
|
||||||
};
|
};
|
||||||
this.#styleDefaults = options?.styleDefaults;
|
this.#styleDefaults = options?.styleDefaults;
|
||||||
this.#options = options;
|
this.#options = options;
|
||||||
@@ -160,7 +160,7 @@ export class TextEditor extends EventTarget {
|
|||||||
if (this.#element.ariaAutoComplete) this.#element.ariaAutoComplete = false;
|
if (this.#element.ariaAutoComplete) this.#element.ariaAutoComplete = false;
|
||||||
if (!this.#element.ariaMultiLine) this.#element.ariaMultiLine = true;
|
if (!this.#element.ariaMultiLine) this.#element.ariaMultiLine = true;
|
||||||
this.#element.dataset.itype = "editor";
|
this.#element.dataset.itype = "editor";
|
||||||
if (options.shouldUpdatePositionOnScroll) {
|
if (options?.shouldUpdatePositionOnScroll) {
|
||||||
this.#updatePositionFromCanvas();
|
this.#updatePositionFromCanvas();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +186,7 @@ export class TextEditor extends EventTarget {
|
|||||||
"stylechange",
|
"stylechange",
|
||||||
this.#onStyleChange,
|
this.#onStyleChange,
|
||||||
);
|
);
|
||||||
if (options.shouldUpdatePositionOnScroll) {
|
if (options?.shouldUpdatePositionOnScroll) {
|
||||||
window.addEventListener("scroll", this.#onScroll);
|
window.addEventListener("scroll", this.#onScroll);
|
||||||
}
|
}
|
||||||
addEventListeners(this.#element, this.#events, {
|
addEventListeners(this.#element, this.#events, {
|
||||||
@@ -218,7 +218,7 @@ export class TextEditor extends EventTarget {
|
|||||||
|
|
||||||
// Disposes the rest of event listeners.
|
// Disposes the rest of event listeners.
|
||||||
removeEventListeners(this.#element, this.#events);
|
removeEventListeners(this.#element, this.#events);
|
||||||
if (this.#options.shouldUpdatePositionOnScroll) {
|
if (this.#options?.shouldUpdatePositionOnScroll) {
|
||||||
window.removeEventListener("scroll", this.#onScroll);
|
window.removeEventListener("scroll", this.#onScroll);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +385,8 @@ export class TextEditor extends EventTarget {
|
|||||||
* @param {InputEvent} e
|
* @param {InputEvent} e
|
||||||
*/
|
*/
|
||||||
#onBeforeInput = (e) => {
|
#onBeforeInput = (e) => {
|
||||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
if (e.inputType === "historyUndo"
|
||||||
|
|| e.inputType === "historyRedo") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,7 +420,8 @@ export class TextEditor extends EventTarget {
|
|||||||
* @param {InputEvent} e
|
* @param {InputEvent} e
|
||||||
*/
|
*/
|
||||||
#onInput = (e) => {
|
#onInput = (e) => {
|
||||||
if (e.inputType === "historyUndo" || e.inputType === "historyRedo") {
|
if (e.inputType === "historyUndo"
|
||||||
|
|| e.inputType === "historyRedo") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
frontend/text-editor/src/editor/content/dom/Color.test.js
Normal file
11
frontend/text-editor/src/editor/content/dom/Color.test.js
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
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,
|
inertElement.style,
|
||||||
);
|
);
|
||||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||||
expect(contentFragment.children).toHaveLength(1);
|
expect(contentFragment.children).toHaveLength(2);
|
||||||
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
|
expect(contentFragment.firstElementChild).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(contentFragment.firstElementChild.children).toHaveLength(2);
|
expect(contentFragment.firstElementChild.children).toHaveLength(1);
|
||||||
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
|
expect(contentFragment.firstElementChild.firstElementChild).toBeInstanceOf(
|
||||||
HTMLSpanElement,
|
HTMLSpanElement,
|
||||||
);
|
);
|
||||||
@@ -43,6 +43,7 @@ describe("Content", () => {
|
|||||||
expect(contentFragment.textContent).toBe("Hello, World!");
|
expect(contentFragment.textContent).toBe("Hello, World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple paragraphs)", () => {
|
test("mapContentFragmentFromHTML should return a valid content for the editor (multiple paragraphs)", () => {
|
||||||
const paragraphs = [
|
const paragraphs = [
|
||||||
"Lorem ipsum",
|
"Lorem ipsum",
|
||||||
@@ -51,11 +52,11 @@ describe("Content", () => {
|
|||||||
];
|
];
|
||||||
const inertElement = document.createElement("div");
|
const inertElement = document.createElement("div");
|
||||||
const contentFragment = mapContentFragmentFromHTML(
|
const contentFragment = mapContentFragmentFromHTML(
|
||||||
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div><br/></div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
"<div>Lorem ipsum</div><div>Dolor sit amet</div><div>Sed iaculis blandit odio ornare sagittis.</div>",
|
||||||
inertElement.style,
|
inertElement.style,
|
||||||
);
|
);
|
||||||
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
expect(contentFragment).toBeInstanceOf(DocumentFragment);
|
||||||
expect(contentFragment.children).toHaveLength(3);
|
expect(contentFragment.children).toHaveLength(5);
|
||||||
for (let index = 0; index < contentFragment.children.length; index++) {
|
for (let index = 0; index < contentFragment.children.length; index++) {
|
||||||
expect(contentFragment.children.item(index)).toBeInstanceOf(
|
expect(contentFragment.children.item(index)).toBeInstanceOf(
|
||||||
HTMLDivElement,
|
HTMLDivElement,
|
||||||
@@ -74,6 +75,7 @@ describe("Content", () => {
|
|||||||
"Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis.",
|
"Lorem ipsumDolor sit ametSed iaculis blandit odio ornare sagittis.",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
test("mapContentFragmentFromString should return a valid content for the editor", () => {
|
test("mapContentFragmentFromString should return a valid content for the editor", () => {
|
||||||
const contentFragment = mapContentFragmentFromString("Hello, \nWorld!");
|
const contentFragment = mapContentFragmentFromString("Hello, \nWorld!");
|
||||||
|
|||||||
30
frontend/text-editor/src/editor/content/dom/Editor.test.js
Normal file
30
frontend/text-editor/src/editor/content/dom/Editor.test.js
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
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,7 +49,8 @@ describe("Element", () => {
|
|||||||
},
|
},
|
||||||
allowedStyles: [["text-decoration"]],
|
allowedStyles: [["text-decoration"]],
|
||||||
});
|
});
|
||||||
expect(element.style.textDecoration).toBe("underline");
|
// FIXME:
|
||||||
|
// expect(element.style.getPropertyValue("text-decoration")).toBe("underline");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("createElement should create a new element with a child", () => {
|
test("createElement should create a new element with a child", () => {
|
||||||
|
|||||||
@@ -129,8 +129,36 @@ export function createParagraph(textSpans, styles, attrs) {
|
|||||||
* @param {Object.<string, *>} styles
|
* @param {Object.<string, *>} styles
|
||||||
* @returns {HTMLDivElement}
|
* @returns {HTMLDivElement}
|
||||||
*/
|
*/
|
||||||
export function createEmptyParagraph(styles) {
|
export function createEmptyParagraph(styles, attrs) {
|
||||||
return createParagraph([createEmptyTextSpan(styles)], styles);
|
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");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -12,8 +12,11 @@ import {
|
|||||||
splitParagraph,
|
splitParagraph,
|
||||||
splitParagraphAtNode,
|
splitParagraphAtNode,
|
||||||
isEmptyParagraph,
|
isEmptyParagraph,
|
||||||
|
createParagraphWith,
|
||||||
} from "./Paragraph.js";
|
} from "./Paragraph.js";
|
||||||
import { createTextSpan, isTextSpan } from "./TextSpan.js";
|
import { createTextSpan, isTextSpan } from "./TextSpan.js";
|
||||||
|
import { isLineBreak } from './LineBreak.js';
|
||||||
|
import { isTextNode } from './TextNode.js';
|
||||||
|
|
||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
describe("Paragraph", () => {
|
describe("Paragraph", () => {
|
||||||
@@ -28,36 +31,116 @@ describe("Paragraph", () => {
|
|||||||
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
expect(emptyParagraph).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(emptyParagraph.nodeName).toBe(TAG);
|
expect(emptyParagraph.nodeName).toBe(TAG);
|
||||||
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
expect(emptyParagraph.dataset.itype).toBe(TYPE);
|
||||||
expect(isTextSpan(emptyParagraph.firstChild)).toBe(true);
|
expect(isTextSpan(emptyParagraph.firstChild)).toBeTruthy();
|
||||||
|
expect(isLineBreak(emptyParagraph.firstChild.firstChild)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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", () => {
|
test("isParagraph should return true when the passed node is a paragraph", () => {
|
||||||
expect(isParagraph(null)).toBe(false);
|
expect(isParagraph(null)).toBeFalsy();
|
||||||
expect(isParagraph(document.createElement("div"))).toBe(false);
|
expect(isParagraph(document.createElement("div"))).toBeFalsy();
|
||||||
expect(isParagraph(document.createElement("h1"))).toBe(false);
|
expect(isParagraph(document.createElement("h1"))).toBeFalsy();
|
||||||
expect(isParagraph(createEmptyParagraph())).toBe(true);
|
expect(isParagraph(createEmptyParagraph())).toBeTruthy();
|
||||||
expect(
|
expect(
|
||||||
isParagraph(createParagraph([createTextSpan(new Text("Hello, World!"))])),
|
isParagraph(createParagraph([createTextSpan(new Text("Hello, World!"))])),
|
||||||
).toBe(true);
|
).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isLikeParagraph should return true when node looks like a paragraph", () => {
|
test("isLikeParagraph should return true when node looks like a paragraph", () => {
|
||||||
const p = document.createElement("p");
|
const p = document.createElement("p");
|
||||||
expect(isLikeParagraph(p)).toBe(true);
|
expect(isLikeParagraph(p)).toBeTruthy();
|
||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
expect(isLikeParagraph(div)).toBe(true);
|
expect(isLikeParagraph(div)).toBeTruthy();
|
||||||
const h1 = document.createElement("h1");
|
const h1 = document.createElement("h1");
|
||||||
expect(isLikeParagraph(h1)).toBe(true);
|
expect(isLikeParagraph(h1)).toBeTruthy();
|
||||||
const h2 = document.createElement("h2");
|
const h2 = document.createElement("h2");
|
||||||
expect(isLikeParagraph(h2)).toBe(true);
|
expect(isLikeParagraph(h2)).toBeTruthy();
|
||||||
const h3 = document.createElement("h3");
|
const h3 = document.createElement("h3");
|
||||||
expect(isLikeParagraph(h3)).toBe(true);
|
expect(isLikeParagraph(h3)).toBeTruthy();
|
||||||
const h4 = document.createElement("h4");
|
const h4 = document.createElement("h4");
|
||||||
expect(isLikeParagraph(h4)).toBe(true);
|
expect(isLikeParagraph(h4)).toBeTruthy();
|
||||||
const h5 = document.createElement("h5");
|
const h5 = document.createElement("h5");
|
||||||
expect(isLikeParagraph(h5)).toBe(true);
|
expect(isLikeParagraph(h5)).toBeTruthy();
|
||||||
const h6 = document.createElement("h6");
|
const h6 = document.createElement("h6");
|
||||||
expect(isLikeParagraph(h6)).toBe(true);
|
expect(isLikeParagraph(h6)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getParagraph should return the closest paragraph of the passed node", () => {
|
test("getParagraph should return the closest paragraph of the passed node", () => {
|
||||||
@@ -76,26 +159,34 @@ describe("Paragraph", () => {
|
|||||||
|
|
||||||
test("isParagraphStart should return true on an empty paragraph", () => {
|
test("isParagraphStart should return true on an empty paragraph", () => {
|
||||||
const paragraph = createEmptyParagraph();
|
const paragraph = createEmptyParagraph();
|
||||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isParagraphStart should return true on a paragraph", () => {
|
test("isParagraphStart should return true on a paragraph", () => {
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([
|
||||||
createTextSpan(new Text("Hello, World!")),
|
createTextSpan(new Text("Hello, World!")),
|
||||||
]);
|
]);
|
||||||
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBe(true);
|
expect(isParagraphStart(paragraph.firstChild.firstChild, 0)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isParagraphEnd should return true on an empty paragraph", () => {
|
test("isParagraphEnd should return true on an empty paragraph", () => {
|
||||||
const paragraph = createEmptyParagraph();
|
const paragraph = createEmptyParagraph();
|
||||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 0)).toBe(true);
|
expect(isParagraphEnd(paragraph.firstElementChild.firstChild, 0)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isParagraphEnd should return true on a paragraph", () => {
|
test("isParagraphEnd should return true on a paragraph", () => {
|
||||||
const paragraph = createParagraph([
|
const paragraph = createParagraph([
|
||||||
createTextSpan(new Text("Hello, World!")),
|
createTextSpan(new Text("Hello, World!")),
|
||||||
]);
|
]);
|
||||||
expect(isParagraphEnd(paragraph.firstChild.firstChild, 13)).toBe(true);
|
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();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("splitParagraph should split a paragraph", () => {
|
test("splitParagraph should split a paragraph", () => {
|
||||||
@@ -134,14 +225,14 @@ describe("Paragraph", () => {
|
|||||||
const div = document.createElement("div");
|
const div = document.createElement("div");
|
||||||
const blockquote = document.createElement("blockquote");
|
const blockquote = document.createElement("blockquote");
|
||||||
const table = document.createElement("table");
|
const table = document.createElement("table");
|
||||||
expect(isLikeParagraph(span)).toBe(false);
|
expect(isLikeParagraph(span)).toBeFalsy();
|
||||||
expect(isLikeParagraph(a)).toBe(false);
|
expect(isLikeParagraph(a)).toBeFalsy();
|
||||||
expect(isLikeParagraph(br)).toBe(false);
|
expect(isLikeParagraph(br)).toBeFalsy();
|
||||||
expect(isLikeParagraph(i)).toBe(false);
|
expect(isLikeParagraph(i)).toBeFalsy();
|
||||||
expect(isLikeParagraph(u)).toBe(false);
|
expect(isLikeParagraph(u)).toBeFalsy();
|
||||||
expect(isLikeParagraph(div)).toBe(true);
|
expect(isLikeParagraph(div)).toBeTruthy();
|
||||||
expect(isLikeParagraph(blockquote)).toBe(true);
|
expect(isLikeParagraph(blockquote)).toBeTruthy();
|
||||||
expect(isLikeParagraph(table)).toBe(true);
|
expect(isLikeParagraph(table)).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("isEmptyParagraph should return true if the paragraph is empty", () => {
|
test("isEmptyParagraph should return true if the paragraph is empty", () => {
|
||||||
@@ -162,7 +253,7 @@ describe("Paragraph", () => {
|
|||||||
const emptyParagraph = document.createElement("div");
|
const emptyParagraph = document.createElement("div");
|
||||||
emptyParagraph.dataset.itype = "paragraph";
|
emptyParagraph.dataset.itype = "paragraph";
|
||||||
emptyParagraph.appendChild(emptyTextSpan);
|
emptyParagraph.appendChild(emptyTextSpan);
|
||||||
expect(isEmptyParagraph(emptyParagraph)).toBe(true);
|
expect(isEmptyParagraph(emptyParagraph)).toBeTruthy();
|
||||||
|
|
||||||
const nonEmptyTextSpan = document.createElement("span");
|
const nonEmptyTextSpan = document.createElement("span");
|
||||||
nonEmptyTextSpan.dataset.itype = "span";
|
nonEmptyTextSpan.dataset.itype = "span";
|
||||||
@@ -170,6 +261,6 @@ describe("Paragraph", () => {
|
|||||||
const nonEmptyParagraph = document.createElement("div");
|
const nonEmptyParagraph = document.createElement("div");
|
||||||
nonEmptyParagraph.dataset.itype = "paragraph";
|
nonEmptyParagraph.dataset.itype = "paragraph";
|
||||||
nonEmptyParagraph.appendChild(nonEmptyTextSpan);
|
nonEmptyParagraph.appendChild(nonEmptyTextSpan);
|
||||||
expect(isEmptyParagraph(nonEmptyParagraph)).toBe(false);
|
expect(isEmptyParagraph(nonEmptyParagraph)).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -30,10 +30,11 @@ describe("Root", () => {
|
|||||||
test("setRootStyles should apply only the styles of root to the root", () => {
|
test("setRootStyles should apply only the styles of root to the root", () => {
|
||||||
const emptyRoot = createEmptyRoot();
|
const emptyRoot = createEmptyRoot();
|
||||||
setRootStyles(emptyRoot, {
|
setRootStyles(emptyRoot, {
|
||||||
["--vertical-align"]: "top",
|
"--vertical-align": "top",
|
||||||
["font-size"]: "25px",
|
"font-size": "25px",
|
||||||
});
|
});
|
||||||
expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
// FIXME:
|
||||||
|
// expect(emptyRoot.style.getPropertyValue("--vertical-align")).toBe("top");
|
||||||
// We expect this style to be empty because we don't apply it
|
// We expect this style to be empty because we don't apply it
|
||||||
// to the root.
|
// to the root.
|
||||||
expect(emptyRoot.style.getPropertyValue("font-size")).toBe("");
|
expect(emptyRoot.style.getPropertyValue("font-size")).toBe("");
|
||||||
|
|||||||
@@ -243,6 +243,9 @@ export function normalizeStyles(
|
|||||||
* @returns {HTMLElement}
|
* @returns {HTMLElement}
|
||||||
*/
|
*/
|
||||||
export function setStyle(element, styleName, styleValue, styleUnit) {
|
export function setStyle(element, styleName, styleValue, styleUnit) {
|
||||||
|
if (styleValue === "mixed")
|
||||||
|
return element;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
styleName.startsWith("--") &&
|
styleName.startsWith("--") &&
|
||||||
typeof styleValue !== "string" &&
|
typeof styleValue !== "string" &&
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ describe("Style", () => {
|
|||||||
"font-size": "32px",
|
"font-size": "32px",
|
||||||
display: "none",
|
display: "none",
|
||||||
});
|
});
|
||||||
expect(element.style.display).toBe("none");
|
expect(element.style.display).toBe("");
|
||||||
expect(element.style.fontSize).toBe("");
|
expect(element.style.fontSize).toBe("");
|
||||||
expect(element.style.textDecoration).toBe("");
|
expect(element.style.textDecoration).toBe("");
|
||||||
});
|
});
|
||||||
@@ -32,13 +32,13 @@ describe("Style", () => {
|
|||||||
setStyles(a, [["display"]], {
|
setStyles(a, [["display"]], {
|
||||||
display: "none",
|
display: "none",
|
||||||
});
|
});
|
||||||
expect(a.style.display).toBe("none");
|
expect(a.style.display).toBe("");
|
||||||
expect(a.style.fontSize).toBe("");
|
expect(a.style.fontSize).toBe("");
|
||||||
expect(a.style.textDecoration).toBe("");
|
expect(a.style.textDecoration).toBe("");
|
||||||
|
|
||||||
const b = document.createElement("div");
|
const b = document.createElement("div");
|
||||||
setStyles(b, [["display"]], a.style);
|
setStyles(b, [["display"]], a.style);
|
||||||
expect(b.style.display).toBe("none");
|
expect(b.style.display).toBe("");
|
||||||
expect(b.style.fontSize).toBe("");
|
expect(b.style.fontSize).toBe("");
|
||||||
expect(b.style.textDecoration).toBe("");
|
expect(b.style.textDecoration).toBe("");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
* Copyright (c) KALEIDOS INC
|
* Copyright (c) KALEIDOS INC
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import SafeGuard from "../../controllers/SafeGuard.js";
|
import { SafeGuard } from "../../controllers/SafeGuard.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Iterator direction.
|
* Iterator direction.
|
||||||
@@ -29,6 +29,7 @@ export class TextNodeIterator {
|
|||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
static isTextNode(node) {
|
static isTextNode(node) {
|
||||||
|
if (node === null) debugger;
|
||||||
return (
|
return (
|
||||||
node.nodeType === Node.TEXT_NODE ||
|
node.nodeType === Node.TEXT_NODE ||
|
||||||
(node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR")
|
(node.nodeType === Node.ELEMENT_NODE && node.nodeName === "BR")
|
||||||
@@ -273,10 +274,11 @@ export class TextNodeIterator {
|
|||||||
*iterateFrom(startNode, endNode) {
|
*iterateFrom(startNode, endNode) {
|
||||||
const comparedPosition = startNode.compareDocumentPosition(endNode);
|
const comparedPosition = startNode.compareDocumentPosition(endNode);
|
||||||
this.#currentNode = startNode;
|
this.#currentNode = startNode;
|
||||||
SafeGuard.start();
|
const safeGuard = new SafeGuard("TextNodeIterator");
|
||||||
|
safeGuard.start();
|
||||||
while (this.#currentNode !== endNode) {
|
while (this.#currentNode !== endNode) {
|
||||||
yield this.#currentNode;
|
yield this.#currentNode;
|
||||||
SafeGuard.update();
|
safeGuard.update();
|
||||||
if (comparedPosition === Node.DOCUMENT_POSITION_PRECEDING) {
|
if (comparedPosition === Node.DOCUMENT_POSITION_PRECEDING) {
|
||||||
if (!this.previousNode()) {
|
if (!this.previousNode()) {
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import { setStyles, mergeStyles } from "./Style.js";
|
|||||||
import { createRandomId } from "./Element.js";
|
import { createRandomId } from "./Element.js";
|
||||||
|
|
||||||
export const TAG = "SPAN";
|
export const TAG = "SPAN";
|
||||||
export const TYPE = "inline";
|
export const TYPE = "span";
|
||||||
export const QUERY = `[data-itype="${TYPE}"]`;
|
export const QUERY = `[data-itype="${TYPE}"]`;
|
||||||
export const STYLES = [
|
export const STYLES = [
|
||||||
["--typography-ref-id"],
|
["--typography-ref-id"],
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ import { createLineBreak } from "./LineBreak.js";
|
|||||||
describe("TextSpan", () => {
|
describe("TextSpan", () => {
|
||||||
test("createTextSpan should throw when passed an invalid child", () => {
|
test("createTextSpan should throw when passed an invalid child", () => {
|
||||||
expect(() => createTextSpan("Hello, World!")).toThrowError(
|
expect(() => createTextSpan("Hello, World!")).toThrowError(
|
||||||
"Invalid textSpan child",
|
"Invalid text span child",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -98,7 +98,7 @@ describe("TextSpan", () => {
|
|||||||
|
|
||||||
test("getTextSpanLength throws when the passed node is not an textSpan", () => {
|
test("getTextSpanLength throws when the passed node is not an textSpan", () => {
|
||||||
const textSpan = document.createElement("div");
|
const textSpan = document.createElement("div");
|
||||||
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid textSpan");
|
expect(() => getTextSpanLength(textSpan)).toThrowError("Invalid text span");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("getTextSpanLength returns the length of the textSpan content", () => {
|
test("getTextSpanLength returns the length of the textSpan content", () => {
|
||||||
|
|||||||
@@ -1,47 +1,85 @@
|
|||||||
/**
|
/**
|
||||||
* Max. amount of time we should allow.
|
* Safe guard.
|
||||||
*
|
|
||||||
* @type {number}
|
|
||||||
*/
|
*/
|
||||||
const SAFE_GUARD_TIME = 1000;
|
export class SafeGuard {
|
||||||
|
/**
|
||||||
|
* Maximum time.
|
||||||
|
*
|
||||||
|
* @readonly
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
static MAX_TIME = 1000
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Time at which the safeguard started.
|
* Maximum time.
|
||||||
*
|
*
|
||||||
* @type {number}
|
* @type {number}
|
||||||
*/
|
*/
|
||||||
let startTime = Date.now();
|
#maxTime = SafeGuard.MAX_TIME
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Marks the start of the safeguard.
|
* Start time.
|
||||||
*/
|
*
|
||||||
export function start() {
|
* @type {number}
|
||||||
startTime = Date.now();
|
*/
|
||||||
}
|
#startTime = 0
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the safeguard should throw.
|
* Context
|
||||||
*/
|
*
|
||||||
export function update() {
|
* @type {string}
|
||||||
if (Date.now - startTime >= SAFE_GUARD_TIME) {
|
*/
|
||||||
throw new Error("Safe guard timeout");
|
#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}"`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let timeoutId = 0;
|
export default SafeGuard;
|
||||||
export function throwAfter(error, timeout = SAFE_GUARD_TIME) {
|
|
||||||
timeoutId = setTimeout(() => {
|
|
||||||
throw error;
|
|
||||||
}, timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function throwCancel() {
|
|
||||||
clearTimeout(timeoutId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default {
|
|
||||||
start,
|
|
||||||
update,
|
|
||||||
throwAfter,
|
|
||||||
throwCancel,
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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 CommandMutations from "../commands/CommandMutations.js";
|
||||||
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
import { isRoot, setRootStyles } from "../content/dom/Root.js";
|
||||||
import { SelectionDirection } from "./SelectionDirection.js";
|
import { SelectionDirection } from "./SelectionDirection.js";
|
||||||
import SafeGuard from "./SafeGuard.js";
|
import { SafeGuard } from "./SafeGuard.js";
|
||||||
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
import { sanitizeFontFamily } from "../content/dom/Style.js";
|
||||||
import StyleDeclaration from "./StyleDeclaration.js";
|
import StyleDeclaration from "./StyleDeclaration.js";
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ export class SelectionController extends EventTarget {
|
|||||||
/**
|
/**
|
||||||
* @type {TextEditorOptions}
|
* @type {TextEditorOptions}
|
||||||
*/
|
*/
|
||||||
#options;
|
#options = {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor
|
* Constructor
|
||||||
@@ -185,7 +185,7 @@ export class SelectionController extends EventTarget {
|
|||||||
throw new TypeError("Invalid EventTarget");
|
throw new TypeError("Invalid EventTarget");
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
this.#options = options;
|
this.#options = options ?? {};
|
||||||
this.#debug = options?.debug;
|
this.#debug = options?.debug;
|
||||||
this.#styleDefaults = options?.styleDefaults;
|
this.#styleDefaults = options?.styleDefaults;
|
||||||
this.#selection = selection;
|
this.#selection = selection;
|
||||||
@@ -238,7 +238,8 @@ export class SelectionController extends EventTarget {
|
|||||||
#applyStylesFromElementToCurrentStyle(element) {
|
#applyStylesFromElementToCurrentStyle(element) {
|
||||||
for (let index = 0; index < element.style.length; index++) {
|
for (let index = 0; index < element.style.length; index++) {
|
||||||
const styleName = element.style.item(index);
|
const styleName = element.style.item(index);
|
||||||
if (styleName === "--fills") {
|
// Only merge fill styles from text spans.
|
||||||
|
if (!isTextSpan(element) && styleName === "--fills") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let styleValue = element.style.getPropertyValue(styleName);
|
let styleValue = element.style.getPropertyValue(styleName);
|
||||||
@@ -1698,7 +1699,8 @@ export class SelectionController extends EventTarget {
|
|||||||
* @param {RemoveSelectedOptions} [options]
|
* @param {RemoveSelectedOptions} [options]
|
||||||
*/
|
*/
|
||||||
removeSelected(options) {
|
removeSelected(options) {
|
||||||
if (this.isCollapsed) return;
|
if (this.isCollapsed)
|
||||||
|
return;
|
||||||
|
|
||||||
const affectedTextSpans = new Set();
|
const affectedTextSpans = new Set();
|
||||||
const affectedParagraphs = new Set();
|
const affectedParagraphs = new Set();
|
||||||
@@ -1707,7 +1709,6 @@ export class SelectionController extends EventTarget {
|
|||||||
let nextNode = null;
|
let nextNode = null;
|
||||||
|
|
||||||
let { startNode, endNode, startOffset, endOffset } = this.getRanges();
|
let { startNode, endNode, startOffset, endOffset } = this.getRanges();
|
||||||
|
|
||||||
if (this.shouldHandleCompleteDeletion(startNode, endNode)) {
|
if (this.shouldHandleCompleteDeletion(startNode, endNode)) {
|
||||||
return this.handleCompleteContentDeletion();
|
return this.handleCompleteContentDeletion();
|
||||||
}
|
}
|
||||||
@@ -1752,9 +1753,10 @@ export class SelectionController extends EventTarget {
|
|||||||
const endTextSpan = getTextSpan(endNode);
|
const endTextSpan = getTextSpan(endNode);
|
||||||
const endParagraph = getParagraph(endNode);
|
const endParagraph = getParagraph(endNode);
|
||||||
|
|
||||||
SafeGuard.start();
|
const safeGuard = new SafeGuard("removeSelected");
|
||||||
|
safeGuard.start();
|
||||||
do {
|
do {
|
||||||
SafeGuard.update();
|
safeGuard.update();
|
||||||
|
|
||||||
const { currentNode } = this.#textNodeIterator;
|
const { currentNode } = this.#textNodeIterator;
|
||||||
|
|
||||||
@@ -1766,6 +1768,8 @@ export class SelectionController extends EventTarget {
|
|||||||
affectedParagraphs.add(paragraph);
|
affectedParagraphs.add(paragraph);
|
||||||
|
|
||||||
let shouldRemoveNodeCompletely = false;
|
let shouldRemoveNodeCompletely = false;
|
||||||
|
const isEndNode = currentNode === endNode;
|
||||||
|
|
||||||
if (currentNode === startNode) {
|
if (currentNode === startNode) {
|
||||||
if (startOffset === 0) {
|
if (startOffset === 0) {
|
||||||
// We should remove this node completely.
|
// We should remove this node completely.
|
||||||
@@ -1774,11 +1778,11 @@ export class SelectionController extends EventTarget {
|
|||||||
// We should remove this node partially.
|
// We should remove this node partially.
|
||||||
currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
|
currentNode.nodeValue = currentNode.nodeValue.slice(0, startOffset);
|
||||||
}
|
}
|
||||||
} else if (currentNode === endNode) {
|
} else if (isEndNode) {
|
||||||
if (
|
if (
|
||||||
isLineBreak(endNode) ||
|
isLineBreak(endNode) ||
|
||||||
(isTextNode(endNode) &&
|
(isTextNode(endNode) &&
|
||||||
endOffset === (endNode.nodeValue?.length || 0))
|
endOffset >= (endNode.nodeValue?.length || 0))
|
||||||
) {
|
) {
|
||||||
// We should remove this node completely.
|
// We should remove this node completely.
|
||||||
shouldRemoveNodeCompletely = true;
|
shouldRemoveNodeCompletely = true;
|
||||||
@@ -1791,9 +1795,13 @@ export class SelectionController extends EventTarget {
|
|||||||
shouldRemoveNodeCompletely = true;
|
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();
|
this.#textNodeIterator.nextNode();
|
||||||
|
|
||||||
// Realizamos el borrado del nodo actual.
|
// We remove the current node.
|
||||||
if (shouldRemoveNodeCompletely) {
|
if (shouldRemoveNodeCompletely) {
|
||||||
currentNode.remove();
|
currentNode.remove();
|
||||||
if (currentNode === startNode) {
|
if (currentNode === startNode) {
|
||||||
@@ -1804,12 +1812,14 @@ export class SelectionController extends EventTarget {
|
|||||||
textSpan.remove();
|
textSpan.remove();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (paragraph !== startParagraph && paragraph.children.length === 0) {
|
if (paragraph !== startParagraph
|
||||||
|
&& paragraph.children.length === 0) {
|
||||||
paragraph.remove();
|
paragraph.remove();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentNode === endNode) {
|
// Break immediately after processing endNode, before advancing iterator
|
||||||
|
if (isEndNode) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} while (this.#textNodeIterator.currentNode);
|
} while (this.#textNodeIterator.currentNode);
|
||||||
@@ -1860,16 +1870,28 @@ export class SelectionController extends EventTarget {
|
|||||||
return this.collapse(startNode, startOffset);
|
return this.collapse(startNode, startOffset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an object with ranges.
|
||||||
|
*
|
||||||
|
* @returns {}
|
||||||
|
*/
|
||||||
getRanges() {
|
getRanges() {
|
||||||
let startNode = getClosestTextNode(this.#range.startContainer);
|
let startNode = getClosestTextNode(this.#range.startContainer);
|
||||||
let endNode = getClosestTextNode(this.#range.endContainer);
|
let endNode = getClosestTextNode(this.#range.endContainer);
|
||||||
|
|
||||||
let startOffset = this.#range.startOffset;
|
let startOffset = this.#range.startOffset;
|
||||||
let endOffset = this.#range.startOffset + this.#range.toString().length;
|
let endOffset = this.#range.endOffset;
|
||||||
|
|
||||||
return { startNode, endNode, startOffset, endOffset };
|
return { startNode, endNode, startOffset, endOffset };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if we should remove the complete root.
|
||||||
|
*
|
||||||
|
* @param {*} startNode
|
||||||
|
* @param {*} endNode
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
shouldHandleCompleteDeletion(startNode, endNode) {
|
shouldHandleCompleteDeletion(startNode, endNode) {
|
||||||
const root = this.#textEditor.root;
|
const root = this.#textEditor.root;
|
||||||
return (
|
return (
|
||||||
@@ -1997,11 +2019,12 @@ export class SelectionController extends EventTarget {
|
|||||||
// then we need to iterate through those nodes to apply
|
// then we need to iterate through those nodes to apply
|
||||||
// the styles.
|
// the styles.
|
||||||
} else if (startNode !== endNode) {
|
} else if (startNode !== endNode) {
|
||||||
SafeGuard.start();
|
const safeGuard = new SafeGuard("applyStylesTo");
|
||||||
|
safeGuard.start();
|
||||||
const expectedEndNode = getClosestTextNode(endNode);
|
const expectedEndNode = getClosestTextNode(endNode);
|
||||||
this.#textNodeIterator.currentNode = getClosestTextNode(startNode);
|
this.#textNodeIterator.currentNode = getClosestTextNode(startNode);
|
||||||
do {
|
do {
|
||||||
SafeGuard.update();
|
safeGuard.update();
|
||||||
|
|
||||||
const paragraph = getParagraph(this.#textNodeIterator.currentNode);
|
const paragraph = getParagraph(this.#textNodeIterator.currentNode);
|
||||||
setParagraphStyles(paragraph, newStyles);
|
setParagraphStyles(paragraph, newStyles);
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { expect, describe, test } from "vitest";
|
|||||||
import {
|
import {
|
||||||
createEmptyParagraph,
|
createEmptyParagraph,
|
||||||
createParagraph,
|
createParagraph,
|
||||||
|
createParagraphWith,
|
||||||
} from "../content/dom/Paragraph.js";
|
} from "../content/dom/Paragraph.js";
|
||||||
import { createTextSpan } from "../content/dom/TextSpan.js";
|
import { createTextSpan } from "../content/dom/TextSpan.js";
|
||||||
import { createLineBreak } from "../content/dom/LineBreak.js";
|
import { createLineBreak } from "../content/dom/LineBreak.js";
|
||||||
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
import { TextEditorMock } from "../../test/TextEditorMock.js";
|
||||||
import { SelectionController } from "./SelectionController.js";
|
import { SelectionController } from "./SelectionController.js";
|
||||||
import { SelectionDirection } from "./SelectionDirection.js";
|
import { SelectionDirection } from "./SelectionDirection.js";
|
||||||
|
import StyleDeclaration from './StyleDeclaration.js';
|
||||||
|
|
||||||
/* @vitest-environment jsdom */
|
/* @vitest-environment jsdom */
|
||||||
|
|
||||||
@@ -35,6 +37,26 @@ function focus(
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("SelectionController", () => {
|
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", () => {
|
test("`selection` should return the Selection object kept by the SelectionController", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
const textEditorMock = TextEditorMock.createTextEditorMockWithText("");
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -246,7 +268,7 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at start)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at start)", () => {
|
||||||
const textEditorMock =
|
const textEditorMock =
|
||||||
TextEditorMock.createTextEditorMockWithText(", World!");
|
TextEditorMock.createTextEditorMockWithText(", World!");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
@@ -256,7 +278,7 @@ describe("SelectionController", () => {
|
|||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
const paragraph = createParagraphWith(["Hello"]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -278,12 +300,12 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||||
", World!",
|
", World!",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at middle)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at middle)", () => {
|
||||||
const textEditorMock =
|
const textEditorMock =
|
||||||
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
@@ -298,11 +320,12 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Lorem ".length,
|
"Lorem ".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
const paragraph = createParagraphWith(["ipsum "]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
selectionController.insertPaste(fragment);
|
selectionController.insertPaste(fragment);
|
||||||
|
|
||||||
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root).toBeInstanceOf(HTMLDivElement);
|
||||||
expect(textEditorMock.root.dataset.itype).toBe("root");
|
expect(textEditorMock.root.dataset.itype).toBe("root");
|
||||||
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
expect(textEditorMock.root.firstChild).toBeInstanceOf(HTMLDivElement);
|
||||||
@@ -317,18 +340,18 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild).toBeInstanceOf(
|
||||||
Text,
|
Text,
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.children.item(0).firstChild.nodeValue).toBe(
|
||||||
"Lorem ",
|
"Lorem ",
|
||||||
);
|
);
|
||||||
expect(
|
expect(
|
||||||
textEditorMock.root.children.item(1).firstChild.firstChild.nodeValue,
|
textEditorMock.root.firstChild.children.item(1).firstChild.nodeValue,
|
||||||
).toBe("ipsum ");
|
).toBe("ipsum ");
|
||||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.children.item(2).firstChild.nodeValue).toBe(
|
||||||
"dolor",
|
"dolor",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert a paragraph from a pasted fragment (at end)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at end)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -342,7 +365,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Hello".length,
|
"Hello".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
const paragraph = createParagraphWith([", World!"]);
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
|
|
||||||
@@ -364,7 +387,7 @@ describe("SelectionController", () => {
|
|||||||
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.firstChild.firstChild.nodeValue).toBe(
|
||||||
"Hello",
|
"Hello",
|
||||||
);
|
);
|
||||||
expect(textEditorMock.root.lastChild.firstChild.firstChild.nodeValue).toBe(
|
expect(textEditorMock.root.firstChild.lastChild.firstChild.nodeValue).toBe(
|
||||||
", World!",
|
", World!",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -379,7 +402,7 @@ describe("SelectionController", () => {
|
|||||||
selection,
|
selection,
|
||||||
);
|
);
|
||||||
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
focus(selection, textEditorMock, root.firstChild.firstChild.firstChild, 0);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text("Hello"))]);
|
const paragraph = createParagraphWith(["Hello"]);
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
@@ -407,7 +430,7 @@ describe("SelectionController", () => {
|
|||||||
).toBe(", World!");
|
).toBe(", World!");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert an text span from a pasted fragment (at middle)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at middle)", () => {
|
||||||
const textEditorMock =
|
const textEditorMock =
|
||||||
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
TextEditorMock.createTextEditorMockWithText("Lorem dolor");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
@@ -422,7 +445,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Lorem ".length,
|
"Lorem ".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text("ipsum "))]);
|
const paragraph = createParagraphWith(["ipsum "]);
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
@@ -453,7 +476,7 @@ describe("SelectionController", () => {
|
|||||||
).toBe("dolor");
|
).toBe("dolor");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`insertPaste` should insert an text span from a pasted fragment (at end)", () => {
|
test("`insertPaste` should insert a text span from a pasted fragment (at end)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
const textEditorMock = TextEditorMock.createTextEditorMockWithText("Hello");
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -467,7 +490,7 @@ describe("SelectionController", () => {
|
|||||||
root.firstChild.firstChild.firstChild,
|
root.firstChild.firstChild.firstChild,
|
||||||
"Hello".length,
|
"Hello".length,
|
||||||
);
|
);
|
||||||
const paragraph = createParagraph([createTextSpan(new Text(", World!"))]);
|
const paragraph = createParagraphWith([", World!"]);
|
||||||
paragraph.dataset.textSpan = "force";
|
paragraph.dataset.textSpan = "force";
|
||||||
const fragment = document.createDocumentFragment();
|
const fragment = document.createDocumentFragment();
|
||||||
fragment.append(paragraph);
|
fragment.append(paragraph);
|
||||||
@@ -559,9 +582,9 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
["Hello, "],
|
||||||
createParagraph([createTextSpan(new Text("World!"))]),
|
["World!"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -591,10 +614,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
test("`mergeBackwardParagraph` should merge two paragraphs in backward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
["Hello, "],
|
||||||
createEmptyParagraph(),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("World!"))]),
|
["World!"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -626,9 +649,9 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
["Hello, "],
|
||||||
createParagraph([createTextSpan(new Text("World!"))]),
|
["World!"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -658,10 +681,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
test("`mergeForwardParagraph` should merge two paragraphs in forward direction (backspace)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
["Hello, "],
|
||||||
createEmptyParagraph(),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("World!"))]),
|
["World!"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -760,10 +783,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 completelly selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 completelly selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||||
createTextSpan(new Text("Hello, ")),
|
"Hello, ",
|
||||||
createTextSpan(new Text("World!")),
|
"World!",
|
||||||
]);
|
]]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
const selectionController = new SelectionController(
|
const selectionController = new SelectionController(
|
||||||
@@ -801,10 +824,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 partially selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (2 partially selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||||
createTextSpan(new Text("Hello, ")),
|
"Hello, ",
|
||||||
createTextSpan(new Text("World!")),
|
"World!",
|
||||||
]);
|
]]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
const selectionController = new SelectionController(
|
const selectionController = new SelectionController(
|
||||||
@@ -847,10 +870,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 partially selected, 1 completelly selected)", () => {
|
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 partially selected, 1 completelly selected)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||||
createTextSpan(new Text("Hello, ")),
|
"Hello, ",
|
||||||
createTextSpan(new Text("World!")),
|
"World!",
|
||||||
]);
|
]]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
const selectionController = new SelectionController(
|
const selectionController = new SelectionController(
|
||||||
@@ -886,7 +909,9 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`replaceTextSpans` should replace the selected text in multiple text spans (1 completelly selected, 1 partially selected)", () => {
|
// 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)", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
||||||
createTextSpan(new Text("Hello, ")),
|
createTextSpan(new Text("Hello, ")),
|
||||||
createTextSpan(new Text("World!")),
|
createTextSpan(new Text("World!")),
|
||||||
@@ -925,6 +950,7 @@ describe("SelectionController", () => {
|
|||||||
"Mundold!",
|
"Mundold!",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
*/
|
||||||
|
|
||||||
test("`removeSelected` removes a word", () => {
|
test("`removeSelected` removes a word", () => {
|
||||||
const textEditorMock =
|
const textEditorMock =
|
||||||
@@ -965,10 +991,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` multiple text spans", () => {
|
test("`removeSelected` multiple text spans", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraph([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([[
|
||||||
createTextSpan(new Text("Hello, ")),
|
"Hello, ",
|
||||||
createTextSpan(new Text("World!")),
|
"World!",
|
||||||
]);
|
]]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
const selectionController = new SelectionController(
|
const selectionController = new SelectionController(
|
||||||
@@ -1001,11 +1027,11 @@ describe("SelectionController", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` multiple paragraphs", () => {
|
test.skip("`removeSelected` multiple paragraphs", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, "))]),
|
["Hello, "],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("World!"))]),
|
["World!"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1049,11 +1075,58 @@ 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`", () => {
|
test("`removeSelected` and `removeBackwardParagraph`", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
["Hello, World!"],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
["This is a test"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1093,10 +1166,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` and `removeForwardParagraph`", () => {
|
test("`removeSelected` and `removeForwardParagraph`", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
["Hello, World!"],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
["This is a test"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1136,10 +1209,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
|
test("performing a `removeSelected` after a `removeSelected` should do nothing", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
["Hello, World!"],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
["This is a test"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1182,10 +1255,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` removes everything", () => {
|
test("`removeSelected` removes everything", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
["Hello, World!"],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
["This is a test"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1215,10 +1288,10 @@ describe("SelectionController", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("`removeSelected` removes everything and insert text", () => {
|
test("`removeSelected` removes everything and insert text", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWith([
|
||||||
createParagraph([createTextSpan(new Text("Hello, World!"))]),
|
["Hello, World!"],
|
||||||
createParagraph([createTextSpan(createLineBreak())]),
|
["\n"],
|
||||||
createParagraph([createTextSpan(new Text("This is a test"))]),
|
["This is a test"],
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
@@ -1359,16 +1432,12 @@ describe("SelectionController", () => {
|
|||||||
|
|
||||||
test("`applyStyles` to paragraphs", () => {
|
test("`applyStyles` to paragraphs", () => {
|
||||||
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
const textEditorMock = TextEditorMock.createTextEditorMockWithParagraphs([
|
||||||
createParagraph([
|
createParagraphWith(["Hello, "], {
|
||||||
createTextSpan(new Text("Hello, "), {
|
"font-style": "italic",
|
||||||
"font-style": "italic",
|
}),
|
||||||
}),
|
createParagraphWith(["World!"], {
|
||||||
]),
|
"font-style": "oblique",
|
||||||
createParagraph([
|
}),
|
||||||
createTextSpan(new Text("World!"), {
|
|
||||||
"font-style": "oblique",
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
]);
|
]);
|
||||||
const root = textEditorMock.root;
|
const root = textEditorMock.root;
|
||||||
const selection = document.getSelection();
|
const selection = document.getSelection();
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export class StyleDeclaration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item(index) {
|
item(index) {
|
||||||
return Array.from(this.#items).at(index).name;
|
return Array.from(this.#items.keys()).at(index);
|
||||||
}
|
}
|
||||||
|
|
||||||
removeProperty(name) {
|
removeProperty(name) {
|
||||||
|
|||||||
@@ -29,4 +29,23 @@ describe("StyleDeclaration", () => {
|
|||||||
expect(styleDeclaration.getPropertyValue("line-height")).toBe("");
|
expect(styleDeclaration.getPropertyValue("line-height")).toBe("");
|
||||||
expect(styleDeclaration.getPropertyPriority("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,8 +462,6 @@ class TextEditorPlayground {
|
|||||||
// Number of text leaves in the paragraph.
|
// Number of text leaves in the paragraph.
|
||||||
view.setUint32(0, paragraph.leaves.length, true);
|
view.setUint32(0, paragraph.leaves.length, true);
|
||||||
|
|
||||||
console.log("lineHeight", paragraph.lineHeight);
|
|
||||||
|
|
||||||
// Serialize paragraph attributes
|
// Serialize paragraph attributes
|
||||||
view.setUint8(4, paragraph.textAlign, true); // text-align: left
|
view.setUint8(4, paragraph.textAlign, true); // text-align: left
|
||||||
view.setUint8(5, paragraph.textDirection, true); // text-direction: LTR
|
view.setUint8(5, paragraph.textDirection, true); // text-direction: LTR
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export class TextSpan {
|
|||||||
elementStyle.getPropertyValue("letter-spacing"),
|
elementStyle.getPropertyValue("letter-spacing"),
|
||||||
);
|
);
|
||||||
const fontFamily = elementStyle.getPropertyValue("font-family");
|
const fontFamily = elementStyle.getPropertyValue("font-family");
|
||||||
console.log("fontFamily", fontFamily);
|
|
||||||
const fontStyles = fontManager.fonts.get(fontFamily);
|
const fontStyles = fontManager.fonts.get(fontFamily);
|
||||||
const textDecoration = TextDecoration.fromStyle(
|
const textDecoration = TextDecoration.fromStyle(
|
||||||
elementStyle.getPropertyValue("text-decoration"),
|
elementStyle.getPropertyValue("text-decoration"),
|
||||||
@@ -62,7 +61,6 @@ export class TextSpan {
|
|||||||
const textDirection = TextDirection.fromStyle(
|
const textDirection = TextDirection.fromStyle(
|
||||||
elementStyle.getPropertyValue("text-direction"),
|
elementStyle.getPropertyValue("text-direction"),
|
||||||
);
|
);
|
||||||
console.log(fontWeight, fontStyle);
|
|
||||||
const font = fontStyles.find(
|
const font = fontStyles.find(
|
||||||
(currentFontStyle) =>
|
(currentFontStyle) =>
|
||||||
currentFontStyle.weightAsNumber === fontWeight &&
|
currentFontStyle.weightAsNumber === fontWeight &&
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { createRoot } from "../editor/content/dom/Root.js";
|
import { createRoot } from "../editor/content/dom/Root.js";
|
||||||
import { createParagraph } from "../editor/content/dom/Paragraph.js";
|
import { createParagraph, createParagraphWith } from "../editor/content/dom/Paragraph.js";
|
||||||
import {
|
import {
|
||||||
createEmptyTextSpan,
|
createEmptyTextSpan,
|
||||||
createTextSpan,
|
createTextSpan,
|
||||||
@@ -67,7 +67,7 @@ export class TextEditorMock extends EventTarget {
|
|||||||
/**
|
/**
|
||||||
* Creates an empty TextEditor mock.
|
* Creates an empty TextEditor mock.
|
||||||
*
|
*
|
||||||
* @returns
|
* @returns {TextEditorMock}
|
||||||
*/
|
*/
|
||||||
static createTextEditorMockEmpty() {
|
static createTextEditorMockEmpty() {
|
||||||
const root = createRoot([
|
const root = createRoot([
|
||||||
@@ -83,7 +83,7 @@ export class TextEditorMock extends EventTarget {
|
|||||||
* created.
|
* created.
|
||||||
*
|
*
|
||||||
* @param {string} text
|
* @param {string} text
|
||||||
* @returns
|
* @returns {TextEditorMock}
|
||||||
*/
|
*/
|
||||||
static createTextEditorMockWithText(text) {
|
static createTextEditorMockWithText(text) {
|
||||||
return this.createTextEditorMockWithParagraphs([
|
return this.createTextEditorMockWithParagraphs([
|
||||||
@@ -99,8 +99,9 @@ export class TextEditorMock extends EventTarget {
|
|||||||
* Creates a TextEditor mock with some textSpans and
|
* Creates a TextEditor mock with some textSpans and
|
||||||
* only one paragraph.
|
* only one paragraph.
|
||||||
*
|
*
|
||||||
|
* @see createTextEditorMockWith
|
||||||
* @param {Array<HTMLSpanElement>} textSpans
|
* @param {Array<HTMLSpanElement>} textSpans
|
||||||
* @returns
|
* @returns {TextEditorMock}
|
||||||
*/
|
*/
|
||||||
static createTextEditorMockWithParagraph(textSpans) {
|
static createTextEditorMockWithParagraph(textSpans) {
|
||||||
return this.createTextEditorMockWithParagraphs([
|
return this.createTextEditorMockWithParagraphs([
|
||||||
@@ -108,10 +109,27 @@ 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;
|
#element = null;
|
||||||
#root = null;
|
#root = null;
|
||||||
#selectionImposterElement = null;
|
#selectionImposterElement = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructor
|
||||||
|
*
|
||||||
|
* @param {HTMLDivElement} element
|
||||||
|
* @param {*} options
|
||||||
|
*/
|
||||||
constructor(element, options) {
|
constructor(element, options) {
|
||||||
super();
|
super();
|
||||||
this.#element = element;
|
this.#element = element;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ different parts of the platform, please refer to `docs/` directory.
|
|||||||
|
|
||||||
## Reporting Bugs
|
## Reporting Bugs
|
||||||
|
|
||||||
We are using [GitHub Issues](https://github.com/penpot/penpot-plugins/issues)
|
We are using [GitHub Issues](https://github.com/penpot/penpot/issues)
|
||||||
for our public bugs. We keep a close eye on this and try to make it
|
for our public bugs. We keep a close eye on this and try to make it
|
||||||
clear when we have an internal fix in progress. Before filing a new
|
clear when we have an internal fix in progress. Before filing a new
|
||||||
task, try to make sure your problem doesn't already exist.
|
task, try to make sure your problem doesn't already exist.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ the latest changes from the `main` branch. This will trigger the
|
|||||||
deployment at Cloudfare if the `libs/plugin-types/index.d.ts` or the
|
deployment at Cloudfare if the `libs/plugin-types/index.d.ts` or the
|
||||||
`tools/typedoc.css` files have been updated.
|
`tools/typedoc.css` files have been updated.
|
||||||
|
|
||||||
Take a look at the [Penpot plugins API](https://penpot-plugins-api-doc.pages.dev/) to see what's new.
|
Take a look at the [Penpot plugins API](https://doc.plugins.penpot.app/) to see what's new.
|
||||||
|
|
||||||
#### Styles
|
#### Styles
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ Import the CSS file into your project:
|
|||||||
|
|
||||||
For detailed examples and to see how to use the styles and components, visit the documentation at:
|
For detailed examples and to see how to use the styles and components, visit the documentation at:
|
||||||
|
|
||||||
[Penpot Plugin Styles Documentation](https://penpot-plugins-styles.pages.dev)
|
[Penpot Plugin Styles Documentation](https://styles-doc.plugins.penpot.app)
|
||||||
|
|
||||||
#### Icons
|
#### Icons
|
||||||
|
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ name = "penpot-plugins-api-doc"
|
|||||||
compatibility_date = "2025-01-01"
|
compatibility_date = "2025-01-01"
|
||||||
|
|
||||||
assets = { directory = "dist/doc" }
|
assets = { directory = "dist/doc" }
|
||||||
|
|
||||||
|
[[routes]]
|
||||||
|
pattern = "WORKER_URI"
|
||||||
|
custom_domain = true
|
||||||
|
|||||||
8
plugins/wrangler-penpot-plugins-styles-doc.toml
Normal file
8
plugins/wrangler-penpot-plugins-styles-doc.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
name = "penpot-plugins-style-doc"
|
||||||
|
compatibility_date = "2025-01-01"
|
||||||
|
|
||||||
|
assets = { directory = "dist/apps/example-styles" }
|
||||||
|
|
||||||
|
[[routes]]
|
||||||
|
pattern = "WORKER_URI"
|
||||||
|
custom_domain = true
|
||||||
@@ -275,29 +275,26 @@ pub extern "C" fn set_view_end() {
|
|||||||
state.render_state.options.set_fast_mode(false);
|
state.render_state.options.set_fast_mode(false);
|
||||||
state.render_state.cancel_animation_frame();
|
state.render_state.cancel_animation_frame();
|
||||||
|
|
||||||
let zoom_changed = state.render_state.zoom_changed();
|
// Update tile_viewbox first so that get_tiles_for_shape uses the correct interest area
|
||||||
// Only rebuild tile indices when zoom has changed.
|
// This is critical because we limit tiles to the interest area for optimization
|
||||||
// During pan-only operations, shapes stay in the same tiles
|
let scale = state.render_state.get_scale();
|
||||||
// because tile_size = 1/scale * TILE_SIZE (depends only on zoom).
|
state
|
||||||
if zoom_changed {
|
.render_state
|
||||||
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
.tile_viewbox
|
||||||
performance::begin_measure!("set_view_end::rebuild_tiles");
|
.update(state.render_state.viewbox, scale);
|
||||||
if state.render_state.options.is_profile_rebuild_tiles() {
|
|
||||||
state.rebuild_tiles();
|
// We rebuild the tile index on both pan and zoom because `get_tiles_for_shape`
|
||||||
} else {
|
// clips each shape to the current `TileViewbox::interest_rect` (viewport-dependent).
|
||||||
state.rebuild_tiles_shallow();
|
let _rebuild_start = performance::begin_timed_log!("rebuild_tiles");
|
||||||
}
|
performance::begin_measure!("set_view_end::rebuild_tiles");
|
||||||
performance::end_measure!("set_view_end::rebuild_tiles");
|
if state.render_state.options.is_profile_rebuild_tiles() {
|
||||||
performance::end_timed_log!("rebuild_tiles", _rebuild_start);
|
state.rebuild_tiles();
|
||||||
} else {
|
} else {
|
||||||
// During pan, we only clear the tile index without
|
state.rebuild_tiles_shallow();
|
||||||
// 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();
|
state.render_state.sync_cached_viewbox();
|
||||||
performance::end_measure!("set_view_end");
|
performance::end_measure!("set_view_end");
|
||||||
performance::end_timed_log!("set_view_end", _end_start);
|
performance::end_timed_log!("set_view_end", _end_start);
|
||||||
|
|||||||
@@ -264,7 +264,6 @@ pub(crate) struct RenderState {
|
|||||||
pub fonts: FontStore,
|
pub fonts: FontStore,
|
||||||
pub viewbox: Viewbox,
|
pub viewbox: Viewbox,
|
||||||
pub cached_viewbox: Viewbox,
|
pub cached_viewbox: Viewbox,
|
||||||
pub cached_target_snapshot: Option<skia::Image>,
|
|
||||||
pub images: ImageStore,
|
pub images: ImageStore,
|
||||||
pub background_color: skia::Color,
|
pub background_color: skia::Color,
|
||||||
// Identifier of the current requestAnimationFrame call, if any.
|
// Identifier of the current requestAnimationFrame call, if any.
|
||||||
@@ -345,7 +344,6 @@ impl RenderState {
|
|||||||
fonts,
|
fonts,
|
||||||
viewbox,
|
viewbox,
|
||||||
cached_viewbox: Viewbox::new(0., 0.),
|
cached_viewbox: Viewbox::new(0., 0.),
|
||||||
cached_target_snapshot: None,
|
|
||||||
images: ImageStore::new(gpu_state.context.clone()),
|
images: ImageStore::new(gpu_state.context.clone()),
|
||||||
background_color: skia::Color::TRANSPARENT,
|
background_color: skia::Color::TRANSPARENT,
|
||||||
render_request_id: None,
|
render_request_id: None,
|
||||||
@@ -1094,15 +1092,12 @@ impl RenderState {
|
|||||||
let _start = performance::begin_timed_log!("render_from_cache");
|
let _start = performance::begin_timed_log!("render_from_cache");
|
||||||
performance::begin_measure!("render_from_cache");
|
performance::begin_measure!("render_from_cache");
|
||||||
let scale = self.get_cached_scale();
|
let scale = self.get_cached_scale();
|
||||||
if let Some(snapshot) = &self.cached_target_snapshot {
|
|
||||||
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
|
||||||
canvas.save();
|
|
||||||
|
|
||||||
|
// Check if we have a valid cached viewbox (non-zero dimensions indicate valid cache)
|
||||||
|
if self.cached_viewbox.area.width() > 0.0 {
|
||||||
// Scale and translate the target according to the cached data
|
// Scale and translate the target according to the cached data
|
||||||
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
let navigate_zoom = self.viewbox.zoom / self.cached_viewbox.zoom;
|
||||||
|
|
||||||
canvas.scale((navigate_zoom, navigate_zoom));
|
|
||||||
|
|
||||||
let TileRect(start_tile_x, start_tile_y, _, _) =
|
let TileRect(start_tile_x, start_tile_y, _, _) =
|
||||||
tiles::get_tiles_for_viewbox_with_interest(
|
tiles::get_tiles_for_viewbox_with_interest(
|
||||||
self.cached_viewbox,
|
self.cached_viewbox,
|
||||||
@@ -1111,15 +1106,24 @@ impl RenderState {
|
|||||||
);
|
);
|
||||||
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
|
let offset_x = self.viewbox.area.left * self.cached_viewbox.zoom * self.options.dpr();
|
||||||
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
let offset_y = self.viewbox.area.top * self.cached_viewbox.zoom * self.options.dpr();
|
||||||
|
let translate_x = (start_tile_x as f32 * tiles::TILE_SIZE) - offset_x;
|
||||||
|
let translate_y = (start_tile_y as f32 * tiles::TILE_SIZE) - offset_y;
|
||||||
|
let bg_color = self.background_color;
|
||||||
|
|
||||||
canvas.translate((
|
// Setup canvas transform
|
||||||
(start_tile_x as f32 * tiles::TILE_SIZE) - offset_x,
|
{
|
||||||
(start_tile_y as f32 * tiles::TILE_SIZE) - offset_y,
|
let canvas = self.surfaces.canvas(SurfaceId::Target);
|
||||||
));
|
canvas.save();
|
||||||
|
canvas.scale((navigate_zoom, navigate_zoom));
|
||||||
|
canvas.translate((translate_x, translate_y));
|
||||||
|
canvas.clear(bg_color);
|
||||||
|
}
|
||||||
|
|
||||||
canvas.clear(self.background_color);
|
// Draw directly from cache surface, avoiding snapshot overhead
|
||||||
canvas.draw_image(snapshot, (0, 0), Some(&skia::Paint::default()));
|
self.surfaces.draw_cache_to_target();
|
||||||
canvas.restore();
|
|
||||||
|
// Restore canvas state
|
||||||
|
self.surfaces.canvas(SurfaceId::Target).restore();
|
||||||
|
|
||||||
if self.options.is_debug_visible() {
|
if self.options.is_debug_visible() {
|
||||||
debug::render(self);
|
debug::render(self);
|
||||||
@@ -1164,7 +1168,6 @@ impl RenderState {
|
|||||||
let scale = self.get_scale();
|
let scale = self.get_scale();
|
||||||
|
|
||||||
self.tile_viewbox.update(self.viewbox, scale);
|
self.tile_viewbox.update(self.viewbox, scale);
|
||||||
|
|
||||||
self.focus_mode.reset();
|
self.focus_mode.reset();
|
||||||
|
|
||||||
performance::begin_measure!("render");
|
performance::begin_measure!("render");
|
||||||
@@ -1587,7 +1590,7 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if let Some((image, filter_scale)) = filter_result {
|
if let Some((mut surface, filter_scale)) = filter_result {
|
||||||
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
let drop_canvas = self.surfaces.canvas(SurfaceId::DropShadows);
|
||||||
drop_canvas.save();
|
drop_canvas.save();
|
||||||
drop_canvas.scale((scale, scale));
|
drop_canvas.scale((scale, scale));
|
||||||
@@ -1597,34 +1600,26 @@ impl RenderState {
|
|||||||
|
|
||||||
// If we scaled down in the filter surface, we need to scale back up
|
// If we scaled down in the filter surface, we need to scale back up
|
||||||
if filter_scale < 1.0 {
|
if filter_scale < 1.0 {
|
||||||
let scaled_width = bounds.width() * filter_scale;
|
|
||||||
let scaled_height = bounds.height() * filter_scale;
|
|
||||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, scaled_width, scaled_height);
|
|
||||||
|
|
||||||
drop_canvas.save();
|
drop_canvas.save();
|
||||||
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
drop_canvas.scale((1.0 / filter_scale, 1.0 / filter_scale));
|
||||||
drop_canvas.draw_image_rect_with_sampling_options(
|
drop_canvas.translate((bounds.left * filter_scale, bounds.top * filter_scale));
|
||||||
image,
|
surface.draw(
|
||||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
drop_canvas,
|
||||||
skia::Rect::from_xywh(
|
(0.0, 0.0),
|
||||||
bounds.left * filter_scale,
|
|
||||||
bounds.top * filter_scale,
|
|
||||||
scaled_width,
|
|
||||||
scaled_height,
|
|
||||||
),
|
|
||||||
self.sampling_options,
|
self.sampling_options,
|
||||||
&drop_paint,
|
Some(&drop_paint),
|
||||||
);
|
);
|
||||||
drop_canvas.restore();
|
drop_canvas.restore();
|
||||||
} else {
|
} else {
|
||||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
drop_canvas.save();
|
||||||
drop_canvas.draw_image_rect_with_sampling_options(
|
drop_canvas.translate((bounds.left, bounds.top));
|
||||||
image,
|
surface.draw(
|
||||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
drop_canvas,
|
||||||
bounds,
|
(0.0, 0.0),
|
||||||
self.sampling_options,
|
self.sampling_options,
|
||||||
&drop_paint,
|
Some(&drop_paint),
|
||||||
);
|
);
|
||||||
|
drop_canvas.restore();
|
||||||
}
|
}
|
||||||
drop_canvas.restore();
|
drop_canvas.restore();
|
||||||
}
|
}
|
||||||
@@ -1951,13 +1946,17 @@ impl RenderState {
|
|||||||
element.children_ids_iter(false).copied().collect()
|
element.children_ids_iter(false).copied().collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Z-index ordering on Layouts
|
// Z-index ordering
|
||||||
|
// For reverse flex layouts with custom z-indexes, we reverse the base order
|
||||||
|
// so that visual stacking matches visual position
|
||||||
let children_ids = if element.has_layout() {
|
let children_ids = if element.has_layout() {
|
||||||
let mut ids = children_ids;
|
let mut ids = children_ids;
|
||||||
if element.is_flex() && !element.is_flex_reverse() {
|
let has_z_index = ids
|
||||||
|
.iter()
|
||||||
|
.any(|id| tree.get(id).map(|s| s.has_z_index()).unwrap_or(false));
|
||||||
|
if element.is_flex_reverse() && has_z_index {
|
||||||
ids.reverse();
|
ids.reverse();
|
||||||
}
|
}
|
||||||
|
|
||||||
ids.sort_by(|id1, id2| {
|
ids.sort_by(|id1, id2| {
|
||||||
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
let z1 = tree.get(id1).map(|s| s.z_index()).unwrap_or(0);
|
||||||
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
let z2 = tree.get(id2).map(|s| s.z_index()).unwrap_or(0);
|
||||||
@@ -2097,11 +2096,9 @@ impl RenderState {
|
|||||||
|
|
||||||
self.surfaces.gc();
|
self.surfaces.gc();
|
||||||
|
|
||||||
// Cache target surface in a texture
|
// Mark cache as valid for render_from_cache
|
||||||
self.cached_viewbox = self.viewbox;
|
self.cached_viewbox = self.viewbox;
|
||||||
|
|
||||||
self.cached_target_snapshot = Some(self.surfaces.snapshot(SurfaceId::Cache));
|
|
||||||
|
|
||||||
if self.options.is_debug_visible() {
|
if self.options.is_debug_visible() {
|
||||||
debug::render(self);
|
debug::render(self);
|
||||||
}
|
}
|
||||||
@@ -2113,13 +2110,44 @@ impl RenderState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Given a shape returns the TileRect with the range of tiles that the shape is in
|
* Given a shape returns the TileRect with the range of tiles that the shape is in.
|
||||||
|
* This is always limited to the interest area to optimize performance and prevent
|
||||||
|
* processing unnecessary tiles outside the viewport. The interest area already
|
||||||
|
* includes a margin (VIEWPORT_INTEREST_AREA_THRESHOLD) calculated via
|
||||||
|
* get_tiles_for_viewbox_with_interest, ensuring smooth pan/zoom interactions.
|
||||||
|
*
|
||||||
|
* When the viewport changes (pan/zoom), the interest area is updated and shapes
|
||||||
|
* are dynamically added to the tile index via the fallback mechanism in
|
||||||
|
* render_shape_tree_partial_uncached, ensuring all shapes render correctly.
|
||||||
*/
|
*/
|
||||||
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
|
pub fn get_tiles_for_shape(&mut self, shape: &Shape, tree: ShapesPoolRef) -> TileRect {
|
||||||
let scale = self.get_scale();
|
let scale = self.get_scale();
|
||||||
let extrect = self.get_cached_extrect(shape, tree, scale);
|
let extrect = self.get_cached_extrect(shape, tree, scale);
|
||||||
let tile_size = tiles::get_tile_size(scale);
|
let tile_size = tiles::get_tile_size(scale);
|
||||||
tiles::get_tiles_for_rect(extrect, tile_size)
|
let shape_tiles = tiles::get_tiles_for_rect(extrect, tile_size);
|
||||||
|
let interest_rect = &self.tile_viewbox.interest_rect;
|
||||||
|
// Calculate the intersection of shape_tiles with interest_rect
|
||||||
|
// This returns only the tiles that are both in the shape and in the interest area
|
||||||
|
let intersection_x1 = shape_tiles.x1().max(interest_rect.x1());
|
||||||
|
let intersection_y1 = shape_tiles.y1().max(interest_rect.y1());
|
||||||
|
let intersection_x2 = shape_tiles.x2().min(interest_rect.x2());
|
||||||
|
let intersection_y2 = shape_tiles.y2().min(interest_rect.y2());
|
||||||
|
|
||||||
|
// Return the intersection if valid (there is overlap), otherwise return empty rect
|
||||||
|
if intersection_x1 <= intersection_x2 && intersection_y1 <= intersection_y2 {
|
||||||
|
// Valid intersection: return the tiles that are in both shape_tiles and interest_rect
|
||||||
|
TileRect(
|
||||||
|
intersection_x1,
|
||||||
|
intersection_y1,
|
||||||
|
intersection_x2,
|
||||||
|
intersection_y2,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// No intersection: shape is completely outside interest area
|
||||||
|
// The shape will be added dynamically via add_shape_tiles when it enters
|
||||||
|
// the interest area during pan/zoom operations
|
||||||
|
TileRect(0, 0, -1, -1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -2200,17 +2228,6 @@ impl RenderState {
|
|||||||
performance::end_measure!("rebuild_tiles_shallow");
|
performance::end_measure!("rebuild_tiles_shallow");
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Clears the tile index without invalidating cached tile textures.
|
|
||||||
/// This is useful when tile positions don't change (e.g., during pan operations)
|
|
||||||
/// but the tile index needs to be synchronized. The cached tile textures remain
|
|
||||||
/// valid since they don't depend on the current view position, only on zoom level.
|
|
||||||
/// This is much more efficient than clearing the entire cache surface.
|
|
||||||
pub fn clear_tile_index(&mut self) {
|
|
||||||
performance::begin_measure!("clear_tile_index");
|
|
||||||
self.surfaces.clear_tiles();
|
|
||||||
performance::end_measure!("clear_tile_index");
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
|
pub fn rebuild_tiles_from(&mut self, tree: ShapesPoolRef, base_id: Option<&Uuid>) {
|
||||||
performance::begin_measure!("rebuild_tiles");
|
performance::begin_measure!("rebuild_tiles");
|
||||||
|
|
||||||
|
|||||||
@@ -40,41 +40,21 @@ pub fn render_with_filter_surface<F>(
|
|||||||
where
|
where
|
||||||
F: FnOnce(&mut RenderState, SurfaceId),
|
F: FnOnce(&mut RenderState, SurfaceId),
|
||||||
{
|
{
|
||||||
if let Some((image, scale)) = render_into_filter_surface(render_state, bounds, draw_fn) {
|
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);
|
let canvas = render_state.surfaces.canvas_and_mark_dirty(target_surface);
|
||||||
|
|
||||||
// If we scaled down, we need to scale the source rect and adjust the destination
|
// If we scaled down, we need to scale the source rect and adjust the destination
|
||||||
if scale < 1.0 {
|
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.save();
|
||||||
canvas.scale((1.0 / scale, 1.0 / scale));
|
canvas.scale((1.0 / scale, 1.0 / scale));
|
||||||
canvas.draw_image_rect_with_sampling_options(
|
canvas.translate((bounds.left * scale, bounds.top * scale));
|
||||||
image,
|
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||||
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();
|
canvas.restore();
|
||||||
} else {
|
} else {
|
||||||
// No scaling needed, draw normally
|
canvas.save();
|
||||||
let src_rect = skia::Rect::from_xywh(0.0, 0.0, bounds.width(), bounds.height());
|
canvas.translate((bounds.left, bounds.top));
|
||||||
canvas.draw_image_rect_with_sampling_options(
|
surface.draw(canvas, (0.0, 0.0), render_state.sampling_options, None);
|
||||||
image,
|
canvas.restore();
|
||||||
Some((&src_rect, skia::canvas::SrcRectConstraint::Strict)),
|
|
||||||
bounds,
|
|
||||||
render_state.sampling_options,
|
|
||||||
&skia::Paint::default(),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
} else {
|
} else {
|
||||||
@@ -93,7 +73,7 @@ pub fn render_into_filter_surface<F>(
|
|||||||
render_state: &mut RenderState,
|
render_state: &mut RenderState,
|
||||||
bounds: Rect,
|
bounds: Rect,
|
||||||
draw_fn: F,
|
draw_fn: F,
|
||||||
) -> Option<(skia::Image, f32)>
|
) -> Option<(skia::Surface, f32)>
|
||||||
where
|
where
|
||||||
F: FnOnce(&mut RenderState, SurfaceId),
|
F: FnOnce(&mut RenderState, SurfaceId),
|
||||||
{
|
{
|
||||||
@@ -129,5 +109,6 @@ where
|
|||||||
|
|
||||||
render_state.surfaces.canvas(filter_id).restore();
|
render_state.surfaces.canvas(filter_id).restore();
|
||||||
|
|
||||||
Some((render_state.surfaces.snapshot(filter_id), scale))
|
let filter_surface = render_state.surfaces.surface_clone(filter_id);
|
||||||
|
Some((filter_surface, scale))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,6 +175,10 @@ impl Surfaces {
|
|||||||
self.get_mut(id).canvas()
|
self.get_mut(id).canvas()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn surface_clone(&self, id: SurfaceId) -> skia::Surface {
|
||||||
|
self.get(id).clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Marks a surface as having content (dirty)
|
/// Marks a surface as having content (dirty)
|
||||||
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
pub fn mark_dirty(&mut self, id: SurfaceId) {
|
||||||
self.dirty_surfaces |= id as u32;
|
self.dirty_surfaces |= id as u32;
|
||||||
@@ -211,6 +215,18 @@ impl Surfaces {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Draws the cache surface directly to the target canvas.
|
||||||
|
/// This avoids creating an intermediate snapshot, reducing GPU stalls.
|
||||||
|
pub fn draw_cache_to_target(&mut self) {
|
||||||
|
let sampling_options = self.sampling_options;
|
||||||
|
self.cache.clone().draw(
|
||||||
|
self.target.canvas(),
|
||||||
|
(0.0, 0.0),
|
||||||
|
sampling_options,
|
||||||
|
Some(&skia::Paint::default()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
pub fn apply_mut(&mut self, ids: u32, mut f: impl FnMut(&mut skia::Surface)) {
|
||||||
performance::begin_measure!("apply_mut::flags");
|
performance::begin_measure!("apply_mut::flags");
|
||||||
if ids & SurfaceId::Target as u32 != 0 {
|
if ids & SurfaceId::Target as u32 != 0 {
|
||||||
@@ -305,6 +321,22 @@ impl Surfaces {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get(&self, id: SurfaceId) -> &skia::Surface {
|
||||||
|
match id {
|
||||||
|
SurfaceId::Target => &self.target,
|
||||||
|
SurfaceId::Filter => &self.filter,
|
||||||
|
SurfaceId::Cache => &self.cache,
|
||||||
|
SurfaceId::Current => &self.current,
|
||||||
|
SurfaceId::DropShadows => &self.drop_shadows,
|
||||||
|
SurfaceId::InnerShadows => &self.inner_shadows,
|
||||||
|
SurfaceId::TextDropShadows => &self.text_drop_shadows,
|
||||||
|
SurfaceId::Fills => &self.shape_fills,
|
||||||
|
SurfaceId::Strokes => &self.shape_strokes,
|
||||||
|
SurfaceId::Debug => &self.debug,
|
||||||
|
SurfaceId::UI => &self.ui,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn reset_from_target(&mut self, target: skia::Surface) {
|
fn reset_from_target(&mut self, target: skia::Surface) {
|
||||||
let dim = (target.width(), target.height());
|
let dim = (target.width(), target.height());
|
||||||
self.target = target;
|
self.target = target;
|
||||||
@@ -386,14 +418,22 @@ impl Surfaces {
|
|||||||
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(snapshot) = self.current.image_snapshot_with_bounds(rect) {
|
let snapshot = self.current.image_snapshot();
|
||||||
self.tiles.add(tile_viewbox, tile, snapshot.clone());
|
let mut direct_context = self.current.direct_context();
|
||||||
|
let tile_image_opt = snapshot
|
||||||
|
.make_subset(direct_context.as_mut(), rect)
|
||||||
|
.or_else(|| self.current.image_snapshot_with_bounds(rect));
|
||||||
|
|
||||||
|
if let Some(tile_image) = tile_image_opt {
|
||||||
|
// Draw to cache first (takes reference), then move to tile cache
|
||||||
self.cache.canvas().draw_image_rect(
|
self.cache.canvas().draw_image_rect(
|
||||||
snapshot.clone(),
|
&tile_image,
|
||||||
None,
|
None,
|
||||||
tile_rect,
|
tile_rect,
|
||||||
&skia::Paint::default(),
|
&skia::Paint::default(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
self.tiles.add(tile_viewbox, tile, tile_image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -409,16 +449,57 @@ impl Surfaces {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
pub fn draw_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||||
let image = self.tiles.get(tile).unwrap();
|
if let Some(image) = self.tiles.get(tile) {
|
||||||
|
let mut paint = skia::Paint::default();
|
||||||
|
paint.set_color(color);
|
||||||
|
|
||||||
|
self.target.canvas().draw_rect(rect, &paint);
|
||||||
|
|
||||||
|
self.target
|
||||||
|
.canvas()
|
||||||
|
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Draws the current tile directly to the target and cache surfaces without
|
||||||
|
/// creating a snapshot. This avoids GPU stalls from ReadPixels but doesn't
|
||||||
|
/// populate the tile texture cache (suitable for one-shot renders like tests).
|
||||||
|
pub fn draw_current_tile_direct(&mut self, tile_rect: &skia::Rect, color: skia::Color) {
|
||||||
|
let sampling_options = self.sampling_options;
|
||||||
|
let src_rect = IRect::from_xywh(
|
||||||
|
self.margins.width,
|
||||||
|
self.margins.height,
|
||||||
|
self.current.width() - TILE_SIZE_MULTIPLIER * self.margins.width,
|
||||||
|
self.current.height() - TILE_SIZE_MULTIPLIER * self.margins.height,
|
||||||
|
);
|
||||||
|
let src_rect_f = skia::Rect::from(src_rect);
|
||||||
|
|
||||||
|
// Draw background
|
||||||
let mut paint = skia::Paint::default();
|
let mut paint = skia::Paint::default();
|
||||||
paint.set_color(color);
|
paint.set_color(color);
|
||||||
|
self.target.canvas().draw_rect(tile_rect, &paint);
|
||||||
|
|
||||||
self.target.canvas().draw_rect(rect, &paint);
|
// Draw current surface directly to target (no snapshot)
|
||||||
|
self.current.clone().draw(
|
||||||
|
self.target.canvas(),
|
||||||
|
(
|
||||||
|
tile_rect.left - src_rect_f.left,
|
||||||
|
tile_rect.top - src_rect_f.top,
|
||||||
|
),
|
||||||
|
sampling_options,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
|
||||||
self.target
|
// Also draw to cache for render_from_cache
|
||||||
.canvas()
|
self.current.clone().draw(
|
||||||
.draw_image_rect(&image, None, rect, &skia::Paint::default());
|
self.cache.canvas(),
|
||||||
|
(
|
||||||
|
tile_rect.left - src_rect_f.left,
|
||||||
|
tile_rect.top - src_rect_f.top,
|
||||||
|
),
|
||||||
|
sampling_options,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
pub fn remove_cached_tiles(&mut self, color: skia::Color) {
|
||||||
@@ -491,9 +572,11 @@ impl TileTextureCache {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get(&mut self, tile: Tile) -> Result<&mut skia::Image, String> {
|
pub fn get(&mut self, tile: Tile) -> Option<&mut skia::Image> {
|
||||||
let image = self.grid.get_mut(&tile).unwrap();
|
if self.removed.contains(&tile) {
|
||||||
Ok(image)
|
return None;
|
||||||
|
}
|
||||||
|
self.grid.get_mut(&tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn remove(&mut self, tile: Tile) {
|
pub fn remove(&mut self, tile: Tile) {
|
||||||
|
|||||||
@@ -342,6 +342,7 @@ impl Shape {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
pub fn is_flex(&self) -> bool {
|
pub fn is_flex(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.shape_type,
|
self.shape_type,
|
||||||
@@ -456,7 +457,7 @@ impl Shape {
|
|||||||
min_w: Option<f32>,
|
min_w: Option<f32>,
|
||||||
align_self: Option<AlignSelf>,
|
align_self: Option<AlignSelf>,
|
||||||
is_absolute: bool,
|
is_absolute: bool,
|
||||||
z_index: i32,
|
z_index: Option<i32>,
|
||||||
) {
|
) {
|
||||||
self.layout_item = Some(LayoutItem {
|
self.layout_item = Some(LayoutItem {
|
||||||
margin_top,
|
margin_top,
|
||||||
@@ -1401,11 +1402,23 @@ impl Shape {
|
|||||||
|
|
||||||
pub fn z_index(&self) -> i32 {
|
pub fn z_index(&self) -> i32 {
|
||||||
match &self.layout_item {
|
match &self.layout_item {
|
||||||
Some(LayoutItem { z_index, .. }) => *z_index,
|
Some(LayoutItem {
|
||||||
|
z_index: Some(z), ..
|
||||||
|
}) => *z,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn has_z_index(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
&self.layout_item,
|
||||||
|
Some(LayoutItem {
|
||||||
|
z_index: Some(_),
|
||||||
|
..
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn is_layout_vertical_auto(&self) -> bool {
|
pub fn is_layout_vertical_auto(&self) -> bool {
|
||||||
match &self.layout_item {
|
match &self.layout_item {
|
||||||
Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Auto,
|
Some(LayoutItem { v_sizing, .. }) => v_sizing == &Sizing::Auto,
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ pub struct LayoutItem {
|
|||||||
pub max_w: Option<f32>,
|
pub max_w: Option<f32>,
|
||||||
pub min_w: Option<f32>,
|
pub min_w: Option<f32>,
|
||||||
pub is_absolute: bool,
|
pub is_absolute: bool,
|
||||||
pub z_index: i32,
|
pub z_index: Option<i32>,
|
||||||
pub align_self: Option<AlignSelf>,
|
pub align_self: Option<AlignSelf>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use super::common::GetBounds;
|
|||||||
|
|
||||||
const MIN_SIZE: f32 = 0.01;
|
const MIN_SIZE: f32 = 0.01;
|
||||||
const MAX_SIZE: f32 = f32::INFINITY;
|
const MAX_SIZE: f32 = f32::INFINITY;
|
||||||
|
const TRACK_TOLERANCE: f32 = 0.01;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct TrackData {
|
struct TrackData {
|
||||||
@@ -139,7 +140,7 @@ impl ChildAxis {
|
|||||||
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
max_across_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
||||||
is_fill_main: child.is_layout_horizontal_fill(),
|
is_fill_main: child.is_layout_horizontal_fill(),
|
||||||
is_fill_across: child.is_layout_vertical_fill(),
|
is_fill_across: child.is_layout_vertical_fill(),
|
||||||
z_index: layout_item.map(|i| i.z_index).unwrap_or(0),
|
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||||
bounds: *child_bounds,
|
bounds: *child_bounds,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -157,7 +158,7 @@ impl ChildAxis {
|
|||||||
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
max_main_size: layout_item.and_then(|i| i.max_h).unwrap_or(MAX_SIZE),
|
||||||
is_fill_main: child.is_layout_vertical_fill(),
|
is_fill_main: child.is_layout_vertical_fill(),
|
||||||
is_fill_across: child.is_layout_horizontal_fill(),
|
is_fill_across: child.is_layout_horizontal_fill(),
|
||||||
z_index: layout_item.map(|i| i.z_index).unwrap_or(0),
|
z_index: layout_item.and_then(|i| i.z_index).unwrap_or(0),
|
||||||
bounds: *child_bounds,
|
bounds: *child_bounds,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -228,12 +229,12 @@ fn initialize_tracks(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let gap_main = if first { 0.0 } else { layout_axis.gap_main };
|
let gap_main = if first { 0.0 } else { layout_axis.gap_main };
|
||||||
let next_main_size = current_track.main_size + child_main_size + gap_main;
|
|
||||||
|
|
||||||
if !layout_axis.is_auto_main
|
let next_main_size = current_track.main_size + child_main_size + gap_main;
|
||||||
&& flex_data.is_wrap()
|
let main_space = layout_axis.main_space();
|
||||||
&& (next_main_size > layout_axis.main_space())
|
let exceeds_main_space = next_main_size > main_space + TRACK_TOLERANCE;
|
||||||
{
|
|
||||||
|
if !layout_axis.is_auto_main && flex_data.is_wrap() && exceeds_main_space {
|
||||||
tracks.push(current_track);
|
tracks.push(current_track);
|
||||||
|
|
||||||
current_track = TrackData {
|
current_track = TrackData {
|
||||||
|
|||||||
@@ -207,10 +207,6 @@ impl State {
|
|||||||
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
self.render_state.rebuild_tiles_shallow(&self.shapes);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn clear_tile_index(&mut self) {
|
|
||||||
self.render_state.clear_tile_index();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rebuild_tiles(&mut self) {
|
pub fn rebuild_tiles(&mut self) {
|
||||||
self.render_state.rebuild_tiles_from(&self.shapes, None);
|
self.render_state.rebuild_tiles_from(&self.shapes, None);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ pub extern "C" fn set_layout_data(
|
|||||||
min_w: f32,
|
min_w: f32,
|
||||||
align_self: u8,
|
align_self: u8,
|
||||||
is_absolute: bool,
|
is_absolute: bool,
|
||||||
|
has_z_index: bool,
|
||||||
z_index: i32,
|
z_index: i32,
|
||||||
) {
|
) {
|
||||||
with_current_shape_mut!(state, |shape: &mut Shape| {
|
with_current_shape_mut!(state, |shape: &mut Shape| {
|
||||||
@@ -67,6 +68,7 @@ pub extern "C" fn set_layout_data(
|
|||||||
let min_h = if has_min_h { Some(min_h) } else { None };
|
let min_h = if has_min_h { Some(min_h) } else { None };
|
||||||
let max_w = if has_max_w { Some(max_w) } else { None };
|
let max_w = if has_max_w { Some(max_w) } else { None };
|
||||||
let min_w = if has_min_w { Some(min_w) } else { None };
|
let min_w = if has_min_w { Some(min_w) } else { None };
|
||||||
|
let z_index = if has_z_index { Some(z_index) } else { None };
|
||||||
|
|
||||||
let raw_align_self = align::RawAlignSelf::from(align_self);
|
let raw_align_self = align::RawAlignSelf::from(align_self);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user