mirror of
https://github.com/penpot/penpot.git
synced 2026-01-27 15:51:32 -05:00
Compare commits
56 Commits
juanfran-t
...
eva-extrac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f02e8467bb | ||
|
|
9e8e4909db | ||
|
|
64a6e51997 | ||
|
|
f6dfac6e1f | ||
|
|
4c823e7c74 | ||
|
|
7a842ce36a | ||
|
|
ea25c5db99 | ||
|
|
ce1796eb02 | ||
|
|
6f0685ba8e | ||
|
|
d433fd25c1 | ||
|
|
bb0e9b47cb | ||
|
|
c5f03d711a | ||
|
|
72cc5ee349 | ||
|
|
804695b48b | ||
|
|
20c8fbf314 | ||
|
|
e02536f8d4 | ||
|
|
3eeaaab17e | ||
|
|
f07495ae95 | ||
|
|
23d5fc7408 | ||
|
|
8632b18eec | ||
|
|
33e650242c | ||
|
|
3dc9e28230 | ||
|
|
e03ad25118 | ||
|
|
d9c56da705 | ||
|
|
75248aec4e | ||
|
|
f0d9429775 | ||
|
|
62ecf48bdb | ||
|
|
18de7f1db6 | ||
|
|
2b2941bd25 | ||
|
|
f2d561eff7 | ||
|
|
418b65a287 | ||
|
|
d4e7810eba | ||
|
|
1d1d32ad39 | ||
|
|
fb08dc65c8 | ||
|
|
927ac93fa7 | ||
|
|
e546a7c614 | ||
|
|
058c20c2e2 | ||
|
|
5d7e6afd76 | ||
|
|
68a77e9cc8 | ||
|
|
e3148ea20e | ||
|
|
5da9bbea62 | ||
|
|
15d369493b | ||
|
|
4ad5282063 | ||
|
|
f94c9cdb02 | ||
|
|
8637c46ba1 | ||
|
|
5d7d23a2c7 | ||
|
|
aab1d97c4c | ||
|
|
499aac31a4 | ||
|
|
962d7839a2 | ||
|
|
83387701a0 | ||
|
|
5775fa61ba | ||
|
|
e0910db99e | ||
|
|
9e0ba4429a | ||
|
|
1f41bef4a9 | ||
|
|
2e400768b7 | ||
|
|
bcaf76d055 |
16
.github/workflows/plugins-deploy-api-doc.yml
vendored
16
.github/workflows/plugins-deploy-api-doc.yml
vendored
@@ -11,7 +11,7 @@ on:
|
||||
- "plugins/libs/plugin-types/REAME.md"
|
||||
- "plugins/tools/typedoc.css"
|
||||
- "plugins/CHANGELOG.md"
|
||||
- "plugins/wrangle-penpot-plugins-api-doc.toml"
|
||||
- "plugins/wrangler-penpot-plugins-api-doc.toml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
@@ -98,4 +98,16 @@ jobs:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
command: deploy --config wrangler-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📚 *[PENPOT PLUGINS] Error deploying API documentation.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
|
||||
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
127
.github/workflows/plugins-deploy-package.yml
vendored
Normal file
@@ -0,0 +1,127 @@
|
||||
name: Plugins/package deployer
|
||||
|
||||
on:
|
||||
# Deploy package from manual action
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
plugin_name:
|
||||
description: 'Pluging name (like plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
workflow_call:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
plugin_name:
|
||||
description: 'Publig name (from plugins/apps/<plugin_name>-plugin)'
|
||||
type: string
|
||||
required: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: penpot-runner-01
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ inputs.gh_ref }}
|
||||
|
||||
# START: Setup Node and PNPM enabling cache
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: .nvmrc
|
||||
|
||||
- name: Enable PNPM
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
corepack enable;
|
||||
corepack install;
|
||||
|
||||
- name: Get pnpm store path
|
||||
id: pnpm-store
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Cache pnpm store
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ${{ steps.pnpm-store.outputs.STORE_PATH }}
|
||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('plugins/pnpm-lock.yaml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pnpm-
|
||||
# END: Setup Node and PNPM enabling cache
|
||||
|
||||
- name: Install deps
|
||||
working-directory: ./plugins
|
||||
shell: bash
|
||||
run: |
|
||||
pnpm install --no-frozen-lockfile;
|
||||
pnpm add -D -w wrangler@latest;
|
||||
|
||||
- name: "Build package for ${{ inputs.plugin_name }}-plugin"
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: npx nx build ${{ inputs.plugin_name }}-plugin
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ inputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pro" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.app" >> $GITHUB_ENV ;;
|
||||
staging)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-pre" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.penpot.dev" >> $GITHUB_ENV ;;
|
||||
develop)
|
||||
echo "WORKER_NAME=${{ inputs.plugin_name }}-plugin-hourly" >> $GITHUB_ENV
|
||||
echo "WORKER_URI=${{ inputs.plugin_name }}.plugins.hourly.penpot.dev" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Set the custom url
|
||||
working-directory: plugins
|
||||
shell: bash
|
||||
run: |
|
||||
sed -i "s/WORKER_URI/${{ env.WORKER_URI }}/g" apps/${{ inputs.plugin_name }}-plugin/wrangler.toml
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
workingDirectory: plugins
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config apps/${{ inputs.plugin_name }}-plugin/wrangler.toml --name ${{ env.WORKER_NAME }}
|
||||
|
||||
- name: Notify Mattermost
|
||||
if: failure()
|
||||
uses: mattermost/action-mattermost-notify@master
|
||||
with:
|
||||
MATTERMOST_WEBHOOK_URL: ${{ secrets.MATTERMOST_WEBHOOK }}
|
||||
MATTERMOST_CHANNEL: bot-alerts-cicd
|
||||
TEXT: |
|
||||
❌ 🧩📦 *[PENPOT PLUGINS] Error deploying ${{ env.WORKER_NAME }}.*
|
||||
📄 Triggered from ref: `${{ inputs.gh_ref }}`
|
||||
Plugin name: `${{ inputs.plugin_name }}-plugin`
|
||||
Cloudflare worker name: `${{ env.WORKER_NAME }}`
|
||||
🔗 Run: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
||||
@infra
|
||||
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
143
.github/workflows/plugins-deploy-packages.yml
vendored
Normal file
@@ -0,0 +1,143 @@
|
||||
name: Plugins/packages deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- 'plugins/apps/*-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch'
|
||||
type: choice
|
||||
required: true
|
||||
default: 'develop'
|
||||
options:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
|
||||
jobs:
|
||||
detect-changes:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
colors_to_tokens: ${{ steps.filter.outputs.colors_to_tokens }}
|
||||
create_palette: ${{ steps.filter.outputs.create_palette }}
|
||||
lorem_ipsum: ${{ steps.filter.outputs.lorem_ipsum }}
|
||||
rename_layers: ${{ steps.filter.outputs.rename_layers }}
|
||||
contrast: ${{ steps.filter.outputs.contrast }}
|
||||
icons: ${{ steps.filter.outputs.icons }}
|
||||
poc_state: ${{ steps.filter.outputs.poc_state }}
|
||||
table: ${{ steps.filter.outputs.table }}
|
||||
# [For new plugins]
|
||||
# Add more outputs here
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- id: filter
|
||||
uses: dorny/paths-filter@v3
|
||||
with:
|
||||
filters: |
|
||||
colors_to_tokens:
|
||||
- 'plugins/apps/colors-to-tokens-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
contrast:
|
||||
- 'plugins/apps/contrast-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
create_palette:
|
||||
- 'plugins/apps/create-palette-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
icons:
|
||||
- 'plugins/apps/icons-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
lorem_ipsum:
|
||||
- 'plugins/apps/lorem-ipsum-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
rename_layers:
|
||||
- 'plugins/apps/rename-layers-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
table:
|
||||
- 'plugins/apps/table-plugin/**'
|
||||
- 'libs/plugins-styles/**'
|
||||
# [For new plugins]
|
||||
# Add more plugin filters here
|
||||
# another_plugin:
|
||||
# - 'plugins/apps/another-plugin/**'
|
||||
# - 'libs/plugins-styles/**'
|
||||
|
||||
colors-to-tokens-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.colors_to_tokens == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: colors-to-tokens
|
||||
|
||||
contrast-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.contrast == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: contrast
|
||||
|
||||
create-palette-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.create_palette == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: create-palette
|
||||
|
||||
icons-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.icons == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: icons
|
||||
|
||||
lorem-ipsum-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.lorem_ipsum == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: lorem-ipsum
|
||||
|
||||
rename-layers-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.rename_layers == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: rename-layers
|
||||
|
||||
table-plugin:
|
||||
needs: detect-changes
|
||||
if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.table == 'true'
|
||||
uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
secrets: inherit
|
||||
with:
|
||||
gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
plugin_name: table
|
||||
|
||||
# [For new plugins]
|
||||
# Add more jobs for other plugins below, following the same pattern
|
||||
# another-plugin:
|
||||
# needs: detect-changes
|
||||
# if: github.event_name == 'workflow_dispatch' || needs.detect-changes.outputs.another_plugin == 'true'
|
||||
# uses: ./.github/workflows/plugins-deploy-package.yml
|
||||
# secrets: inherit
|
||||
# with:
|
||||
# gh_ref: "${{ inputs.gh_ref || github.ref_name }}"
|
||||
# plugin_name: another
|
||||
1
.github/workflows/tests.yml
vendored
1
.github/workflows/tests.yml
vendored
@@ -54,6 +54,7 @@ jobs:
|
||||
test-plugins:
|
||||
name: Plugins Runtime Linter & Tests
|
||||
runs-on: penpot-runner-02
|
||||
container: penpotapp/devenv:latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
@@ -19,13 +19,17 @@
|
||||
|
||||
### :bug: Bugs fixed
|
||||
|
||||
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
|
||||
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
|
||||
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
|
||||
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
|
||||
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
|
||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
|
||||
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
|
||||
- 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 exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||
|
||||
## 2.13.0 (Unreleased)
|
||||
|
||||
@@ -61,6 +65,11 @@
|
||||
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
|
||||
- Fix allow negative spread values on shadow token creation [Taiga #13167](https://tree.taiga.io/project/penpot/issue/13167)
|
||||
- Fix spanish translations on import export token modal [Taiga #13171](https://tree.taiga.io/project/penpot/issue/13171)
|
||||
- Remove whitespaces from asset export filename [Github #8133](https://github.com/penpot/penpot/pull/8133)
|
||||
- Fix exception on uploading large fonts [Github #8135](https://github.com/penpot/penpot/pull/8135)
|
||||
- Fix unhandled exception on open-new-window helper [Github #7787](https://github.com/penpot/penpot/issues/7787)
|
||||
- Fix incorrect handling of input values on layout gap and padding inputs [Github #8113](https://github.com/penpot/penpot/issues/8113)
|
||||
|
||||
|
||||
## 2.12.1
|
||||
|
||||
|
||||
@@ -12,43 +12,22 @@ Debug Main Page
|
||||
</nav>
|
||||
<main class="dashboard">
|
||||
<section class="widget">
|
||||
<fieldset>
|
||||
<legend>Error reports</legend>
|
||||
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>Profile Management</legend>
|
||||
<form method="post" action="/dbg/actions/resend-email-verification">
|
||||
<div class="row">
|
||||
<input type="email" name="email" placeholder="example@example.com" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-verify">Are you sure?</label>
|
||||
<input id="force-verify" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" name="resend" value="Resend Verification" />
|
||||
<input type="submit" name="verify" value="Verify" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" class="danger" name="block" value="Block" />
|
||||
<input type="submit" class="danger" name="unblock" value="Unblock" />
|
||||
</div>
|
||||
</form>
|
||||
<legend>CURRENT PROFILE</legend>
|
||||
<desc>
|
||||
<p>
|
||||
Name: <b>{{profile.fullname}}</b> <br />
|
||||
Email: <b>{{profile.email}}</b>
|
||||
</p>
|
||||
</desc>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>VIRTUAL CLOCK</legend>
|
||||
|
||||
<desc>
|
||||
<p><b>IMPORTANT:</b> The virtual clock is profile based and only affects the currently logged-in profile.</p>
|
||||
<p>
|
||||
CURRENT CLOCK: <b>{{current-clock}}</b>
|
||||
<br />
|
||||
@@ -81,8 +60,93 @@ Debug Main Page
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
<fieldset>
|
||||
<legend>ERROR REPORTS</legend>
|
||||
<desc><a href="/dbg/error">CLICK HERE TO SEE THE ERROR REPORTS</a> </desc>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="widget">
|
||||
<fieldset>
|
||||
<legend>Profile Management</legend>
|
||||
<form method="post" action="/dbg/actions/resend-email-verification">
|
||||
<div class="row">
|
||||
<input type="email" name="email" placeholder="example@example.com" value="" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-verify">Are you sure?</label>
|
||||
<input id="force-verify" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" name="resend" value="Resend Verification" />
|
||||
<input type="submit" name="verify" value="Verify" />
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" class="danger" name="block" value="Block" />
|
||||
<input type="submit" class="danger" name="unblock" value="Unblock" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
|
||||
|
||||
<fieldset>
|
||||
<legend>Feature Flags for Team</legend>
|
||||
<desc>Add a feature flag to a team</desc>
|
||||
<form method="post" action="/dbg/actions/handle-team-features">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<select type="text" style="width:100px" name="feature">
|
||||
{% for feature in supported-features %}
|
||||
<option value="{{feature}}">{{feature}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<select style="width:100px" name="action">
|
||||
<option value="">Action...</option>
|
||||
<option value="show">Show</option>
|
||||
<option value="enable">Enable</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
|
||||
<section class="widget">
|
||||
|
||||
<fieldset>
|
||||
@@ -173,55 +237,5 @@ Debug Main Page
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
|
||||
<section class="widget">
|
||||
<fieldset>
|
||||
<legend>Feature Flags for Team</legend>
|
||||
<desc>Add a feature flag to a team</desc>
|
||||
<form method="post" action="/dbg/actions/handle-team-features">
|
||||
<div class="row">
|
||||
<input type="text" style="width:300px" name="team-id" placeholder="team-id" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<select type="text" style="width:100px" name="feature">
|
||||
{% for feature in supported-features %}
|
||||
<option value="{{feature}}">{{feature}}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<select style="width:100px" name="action">
|
||||
<option value="">Action...</option>
|
||||
<option value="show">Show</option>
|
||||
<option value="enable">Enable</option>
|
||||
<option value="disable">Disable</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="check-feature">Skip feature check</label>
|
||||
<input id="check-feature" type="checkbox" name="skip-check" />
|
||||
<br />
|
||||
<small>
|
||||
Do not check if the feature is supported
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<label for="force-version">Are you sure?</label>
|
||||
<input id="force-version" type="checkbox" name="force" />
|
||||
<br />
|
||||
<small>
|
||||
This is a just a security double check for prevent non intentional submits.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<input type="submit" value="Submit" />
|
||||
</div>
|
||||
</form>
|
||||
</fieldset>
|
||||
</section>
|
||||
</main>
|
||||
{% endblock %}
|
||||
|
||||
@@ -49,13 +49,16 @@
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
(defn index-handler
|
||||
[_cfg _request]
|
||||
(let [{:keys [clock offset]} @clock/current]
|
||||
[cfg request]
|
||||
(let [profile-id (::session/profile-id request)
|
||||
offset (clock/get-offset profile-id)
|
||||
profile (profile/get-profile cfg profile-id)]
|
||||
{::yres/status 200
|
||||
::yres/headers {"content-type" "text/html"}
|
||||
::yres/body (-> (io/resource "app/templates/debug.tmpl")
|
||||
(tmpl/render {:version (:full cf/version)
|
||||
:current-clock (str clock)
|
||||
:profile profile
|
||||
:current-clock ct/*clock*
|
||||
:current-offset (if offset
|
||||
(ct/format-duration offset)
|
||||
"NO OFFSET")
|
||||
@@ -447,15 +450,16 @@
|
||||
|
||||
(defn- set-virtual-clock
|
||||
[_ {:keys [params] :as request}]
|
||||
(let [offset (some-> params :offset str/trim not-empty ct/duration)
|
||||
reset? (contains? params :reset)]
|
||||
(let [offset (some-> params :offset str/trim not-empty ct/duration)
|
||||
profile-id (::session/profile-id request)
|
||||
reset? (contains? params :reset)]
|
||||
(if (= "production" (cf/get :tenant))
|
||||
{::yres/status 501
|
||||
::yres/body "OPERATION NOT ALLOWED"}
|
||||
(do
|
||||
(if (or reset? (zero? (inst-ms offset)))
|
||||
(clock/set-offset! nil)
|
||||
(clock/set-offset! offset))
|
||||
(clock/assign-offset profile-id nil)
|
||||
(clock/assign-offset profile-id offset))
|
||||
{::yres/status 302
|
||||
::yres/headers {"location" "/dbg"}}))))
|
||||
|
||||
@@ -495,7 +499,7 @@
|
||||
|
||||
(defn authorized?
|
||||
[pool {:keys [::session/profile-id]}]
|
||||
(or (= "devenv" (cf/get :host))
|
||||
(or (and (= "devenv" (cf/get :host)) profile-id)
|
||||
(let [profile (ex/ignoring (profile/get-profile pool profile-id))
|
||||
admins (or (cf/get :admins) #{})]
|
||||
(contains? admins (:email profile)))))
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[app.http.session.tasks :as-alias tasks]
|
||||
[app.main :as-alias main]
|
||||
[app.setup :as-alias setup]
|
||||
[app.setup.clock :as clock]
|
||||
[app.tokens :as tokens]
|
||||
[integrant.core :as ig]
|
||||
[yetti.request :as yreq]
|
||||
@@ -229,18 +230,22 @@
|
||||
(let [{:keys [type token claims metadata]} (get request ::http/auth-data)]
|
||||
(cond
|
||||
(= type :cookie)
|
||||
(let [session (case (:ver metadata)
|
||||
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
|
||||
0 (read-session manager token)
|
||||
1 (some->> (:sid claims) (read-session manager))
|
||||
nil)
|
||||
(let [session
|
||||
(case (:ver metadata)
|
||||
;; BACKWARD COMPATIBILITY WITH OLD TOKENS
|
||||
0 (read-session manager token)
|
||||
1 (some->> (:sid claims) (read-session manager))
|
||||
nil)
|
||||
|
||||
request (cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::session session)))
|
||||
request
|
||||
(cond-> request
|
||||
(some? session)
|
||||
(-> (assoc ::profile-id (:profile-id session))
|
||||
(assoc ::session session)))
|
||||
|
||||
response (handler request)]
|
||||
response
|
||||
(binding [ct/*clock* (clock/get-clock (:profile-id session))]
|
||||
(handler request))]
|
||||
|
||||
(if (and session (renew-session? session))
|
||||
(let [session (->> session
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.json :as json]
|
||||
[app.common.logging :as l]
|
||||
[app.common.schema :as sm]
|
||||
[app.common.time :as ct]
|
||||
@@ -223,7 +224,7 @@
|
||||
(some? tnow)
|
||||
(assoc :tracked-at tnow))))
|
||||
|
||||
(defn- append-audit-entry!
|
||||
(defn- append-audit-entry
|
||||
[cfg params]
|
||||
(let [params (-> params
|
||||
(update :props db/tjson)
|
||||
@@ -236,6 +237,16 @@
|
||||
(let [params (event->params event)
|
||||
tnow (ct/now)]
|
||||
|
||||
(when (contains? cf/flags :audit-log-logger)
|
||||
(l/log! ::l/logger "app.audit"
|
||||
::l/level :info
|
||||
:profile-id (str (::profile-id event))
|
||||
:ip-addr (str (::ip-addr event))
|
||||
:type (::type event)
|
||||
:name (::name event)
|
||||
:props (json/encode (::props event) :key-fn json/write-camel-key)
|
||||
:context (json/encode (::context event) :key-fn json/write-camel-key)))
|
||||
|
||||
(when (contains? cf/flags :audit-log)
|
||||
;; NOTE: this operation may cause primary key conflicts on inserts
|
||||
;; because of the timestamp precission (two concurrent requests), in
|
||||
@@ -243,7 +254,7 @@
|
||||
(let [params (-> params
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow)))]
|
||||
(append-audit-entry! cfg params)))
|
||||
(append-audit-entry cfg params)))
|
||||
|
||||
(when (and (or (contains? cf/flags :telemetry)
|
||||
(cf/get :telemetry-enabled))
|
||||
@@ -258,7 +269,7 @@
|
||||
(update :tracked-at #(or % tnow))
|
||||
(assoc :props {})
|
||||
(assoc :context {}))]
|
||||
(append-audit-entry! cfg params)))
|
||||
(append-audit-entry cfg params)))
|
||||
|
||||
(when (and (contains? cf/flags :webhooks)
|
||||
(::webhooks/event? event))
|
||||
@@ -312,4 +323,4 @@
|
||||
params (-> (event->params event)
|
||||
(assoc :created-at tnow)
|
||||
(update :tracked-at #(or % tnow)))]
|
||||
(append-audit-entry! cfg params)))))))
|
||||
(append-audit-entry cfg params)))))))
|
||||
|
||||
@@ -27,7 +27,17 @@
|
||||
[app.rpc.helpers :as rph]
|
||||
[app.rpc.quotes :as quotes]
|
||||
[app.storage :as sto]
|
||||
[app.util.services :as sv]))
|
||||
[app.storage.tmp :as tmp]
|
||||
[app.util.services :as sv]
|
||||
[datoteka.io :as io])
|
||||
(:import
|
||||
java.io.InputStream
|
||||
java.io.OutputStream
|
||||
java.io.SequenceInputStream
|
||||
java.util.Collections))
|
||||
|
||||
(set! *warn-on-reflection* true)
|
||||
|
||||
|
||||
(def valid-weight #{100 200 300 400 500 600 700 800 900 950})
|
||||
(def valid-style #{"normal" "italic"})
|
||||
@@ -105,7 +115,7 @@
|
||||
|
||||
(defn create-font-variant
|
||||
[{:keys [::sto/storage ::db/conn]} {:keys [data] :as params}]
|
||||
(letfn [(generate-missing! [data]
|
||||
(letfn [(generate-missing [data]
|
||||
(let [data (media/run {:cmd :generate-fonts :input data})]
|
||||
(when (and (not (contains? data "font/otf"))
|
||||
(not (contains? data "font/ttf"))
|
||||
@@ -116,8 +126,26 @@
|
||||
:hint "invalid font upload, unable to generate missing font assets"))
|
||||
data))
|
||||
|
||||
(process-chunks [chunks]
|
||||
(let [tmp (tmp/tempfile :prefix "penpot.tempfont." :suffix "")
|
||||
streams (map io/input-stream chunks)
|
||||
streams (Collections/enumeration streams)]
|
||||
(with-open [^OutputStream output (io/output-stream tmp)
|
||||
^InputStream input (SequenceInputStream. streams)]
|
||||
(io/copy input output))
|
||||
tmp))
|
||||
|
||||
(join-chunks [data]
|
||||
(reduce-kv (fn [data mtype content]
|
||||
(if (vector? content)
|
||||
(assoc data mtype (process-chunks content))
|
||||
data))
|
||||
data
|
||||
data))
|
||||
|
||||
(prepare-font [data mtype]
|
||||
(when-let [resource (get data mtype)]
|
||||
|
||||
(let [hash (sto/calculate-hash resource)
|
||||
content (-> (sto/content resource)
|
||||
(sto/wrap-with-hash hash))]
|
||||
@@ -156,7 +184,8 @@
|
||||
:otf-file-id (:id otf)
|
||||
:ttf-file-id (:id ttf)}))]
|
||||
|
||||
(let [data (generate-missing! data)
|
||||
(let [data (join-chunks data)
|
||||
data (generate-missing data)
|
||||
assets (persist-fonts-files! data)
|
||||
result (insert-font-variant! assets)]
|
||||
(vary-meta result assoc ::audit/replace-props (update params :data (comp vec keys))))))
|
||||
|
||||
@@ -9,48 +9,35 @@
|
||||
modification of time offset (useful for testing and time adjustments)."
|
||||
(:require
|
||||
[app.common.logging :as l]
|
||||
[app.common.time :as ct]
|
||||
[app.setup :as-alias setup]
|
||||
[integrant.core :as ig])
|
||||
(:import
|
||||
java.time.Clock
|
||||
java.time.Duration
|
||||
java.time.Instant
|
||||
java.time.ZoneId))
|
||||
[app.common.time :as ct]))
|
||||
|
||||
(defonce current
|
||||
(atom {:clock (Clock/systemDefaultZone)
|
||||
:offset nil}))
|
||||
(defonce state
|
||||
(atom {}))
|
||||
|
||||
(defmethod ig/init-key ::setup/clock
|
||||
[_ _]
|
||||
(add-watch current ::common
|
||||
(fn [_ _ _ {:keys [clock offset]}]
|
||||
(let [clock (if (ct/duration? offset)
|
||||
(Clock/offset ^Clock clock
|
||||
^Duration offset)
|
||||
clock)]
|
||||
(l/wrn :hint "altering clock" :clock (str clock))
|
||||
(alter-var-root #'ct/*clock* (constantly clock))))))
|
||||
(defn assign-offset
|
||||
"Assign virtual clock offset to a specific user. Is the responsability
|
||||
of RPC module to properly bind the correct clock to the user
|
||||
request."
|
||||
[profile-id duration]
|
||||
(swap! state (fn [state]
|
||||
(if (nil? duration)
|
||||
(dissoc state profile-id)
|
||||
(assoc state profile-id duration)))))
|
||||
|
||||
(defn get-offset
|
||||
[profile-id]
|
||||
(get @state profile-id))
|
||||
|
||||
(defmethod ig/halt-key! ::setup/clock
|
||||
[_ _]
|
||||
(remove-watch current ::common))
|
||||
(defn get-clock
|
||||
[profile-id]
|
||||
(if-let [offset (get-offset profile-id)]
|
||||
(ct/offset-clock offset)
|
||||
(ct/get-system-clock)))
|
||||
|
||||
(defn fixed
|
||||
"Get fixed clock, mainly used in tests"
|
||||
[instant]
|
||||
(Clock/fixed ^Instant (ct/inst instant)
|
||||
^ZoneId (ZoneId/of "Z")))
|
||||
|
||||
(defn set-offset!
|
||||
[duration]
|
||||
(swap! current assoc :offset (some-> duration ct/duration)))
|
||||
|
||||
(defn set-clock!
|
||||
(defn set-global-clock
|
||||
([]
|
||||
(swap! current assoc :clock (Clock/systemDefaultZone)))
|
||||
(set-global-clock (ct/get-system-clock)))
|
||||
([clock]
|
||||
(when (instance? Clock clock)
|
||||
(swap! current assoc :clock clock))))
|
||||
(assert (ct/clock? clock) "expected valid clock instance")
|
||||
(l/wrn :hint "altering clock" :clock (str clock))
|
||||
(alter-var-root #'ct/*clock* (constantly clock))))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
[app.db.sql :as sql]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup.clock :as clock]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
@@ -134,7 +133,7 @@
|
||||
;; this will run pending task triggered by deleting user snapshot
|
||||
(th/run-pending-tasks!)
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [res (th/run-task! :objects-gc {})]
|
||||
;; delete 2 snapshots and 2 file data entries
|
||||
(t/is (= 4 (:processed res)))))))))
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.rpc.commands.files :as files]
|
||||
[app.setup.clock :as clock]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
@@ -922,7 +921,7 @@
|
||||
(t/is (= 0 (:processed result))))
|
||||
|
||||
;; run permanent deletion
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [result (th/run-task! :objects-gc {})]
|
||||
(t/is (= 3 (:processed result)))))
|
||||
|
||||
@@ -1875,7 +1874,7 @@
|
||||
file-id (uuid/next)
|
||||
now (ct/inst "2025-10-31T00:00:00Z")]
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||
(let [data {::th/type :create-file
|
||||
::rpc/profile-id (:id prof)
|
||||
:project-id proj-id
|
||||
@@ -1937,7 +1936,7 @@
|
||||
file-id (uuid/next)
|
||||
now (ct/inst "2025-10-31T00:00:00Z")]
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||
(let [data {::th/type :create-file
|
||||
::rpc/profile-id (:id prof)
|
||||
:project-id proj-id
|
||||
@@ -2000,7 +1999,7 @@
|
||||
team-id (:default-team-id profile)
|
||||
now (ct/inst "2025-10-31T00:00:00Z")]
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||
(let [project (th/create-project* 1 {:profile-id (:id profile)
|
||||
:team-id team-id})
|
||||
file (th/create-file* 1 {:profile-id (:id profile)
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
(t/is (map? (:result out))))
|
||||
|
||||
;; run the task again
|
||||
(let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
|
||||
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))]
|
||||
(th/run-task! "storage-gc-touched" {}))]
|
||||
(t/is (= 2 (:freeze res))))
|
||||
|
||||
@@ -136,7 +136,7 @@
|
||||
(t/is (some? (sto/get-object storage (:media-id row2))))
|
||||
|
||||
;; run the task again
|
||||
(let [res (binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 31}))]
|
||||
(let [res (binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 31}))]
|
||||
(th/run-task! :storage-gc-touched {}))]
|
||||
(t/is (= 1 (:delete res)))
|
||||
(t/is (= 0 (:freeze res))))
|
||||
@@ -147,7 +147,7 @@
|
||||
|
||||
;; Run the storage gc deleted task, it should permanently delete
|
||||
;; all storage objects related to the deleted thumbnails
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 1 (:deleted res)))))
|
||||
|
||||
@@ -247,7 +247,7 @@
|
||||
|
||||
;; Run the storage gc deleted task, it should permanently delete
|
||||
;; all storage objects related to the deleted thumbnails
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [result (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 1 (:deleted result)))))
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup.clock :as clock]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
@@ -147,7 +146,7 @@
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [res (th/run-task! :objects-gc {})]
|
||||
(t/is (= 2 (:processed res))))
|
||||
|
||||
@@ -208,7 +207,7 @@
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [res (th/run-task! :objects-gc {})]
|
||||
(t/is (= 1 (:processed res))))
|
||||
|
||||
@@ -268,7 +267,7 @@
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [res (th/run-task! :objects-gc {})]
|
||||
(t/is (= 1 (:processed res))))
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup.clock :as clock]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]))
|
||||
|
||||
@@ -228,7 +227,7 @@
|
||||
(t/is (= 0 (count result)))))
|
||||
|
||||
;; run permanent deletion
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [result (th/run-task! :objects-gc {})]
|
||||
(t/is (= 1 (:processed result)))))
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.db :as db]
|
||||
[app.http :as http]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup.clock :as clock]
|
||||
[app.storage :as sto]
|
||||
[app.tokens :as tokens]
|
||||
[backend-tests.helpers :as th]
|
||||
@@ -526,7 +525,7 @@
|
||||
(t/is (= :not-found (:type edata)))))
|
||||
|
||||
;; run permanent deletion
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [result (th/run-task! :objects-gc {})]
|
||||
(t/is (= 2 (:processed result)))))
|
||||
|
||||
@@ -583,7 +582,7 @@
|
||||
(t/is (= 1 (count rows)))
|
||||
(t/is (ct/inst? (:deleted-at (first rows)))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:days 8}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:days 8}))]
|
||||
(let [result (th/run-task! :objects-gc {})]
|
||||
(t/is (= 7 (:processed result)))))))
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.uuid :as uuid]
|
||||
[app.db :as db]
|
||||
[app.rpc :as-alias rpc]
|
||||
[app.setup.clock :as clock]
|
||||
[app.storage :as sto]
|
||||
[backend-tests.helpers :as th]
|
||||
[clojure.test :as t]
|
||||
@@ -99,14 +98,14 @@
|
||||
::sto/expired-at (ct/in-future {:hours 1})
|
||||
:content-type "text/plain"})]
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 0}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 0}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 1 (:deleted res)))))
|
||||
|
||||
(let [res (th/db-exec-one! ["select count(*) from storage_object;"])]
|
||||
(t/is (= 2 (:count res))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/in-future {:minutes 61}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/in-future {:minutes 61}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 1 (:deleted res)))))
|
||||
|
||||
@@ -331,22 +330,22 @@
|
||||
:content-type "text/plain"})]
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed now)]
|
||||
(binding [ct/*clock* (ct/fixed-clock now)]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 0 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:minutes 1}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:minutes 1}))]
|
||||
(let [res (th/run-task! :storage-gc-touched {})]
|
||||
(t/is (= 0 (:freeze res)))
|
||||
(t/is (= 1 (:delete res)))))
|
||||
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 1}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 1}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))
|
||||
|
||||
(binding [ct/*clock* (clock/fixed (ct/plus now {:hours 2}))]
|
||||
(binding [ct/*clock* (ct/fixed-clock (ct/plus now {:hours 2}))]
|
||||
(let [res (th/run-task! :storage-gc-deleted {})]
|
||||
(t/is (= 0 (:deleted res)))))))
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#{:audit-log
|
||||
:audit-log-archive
|
||||
:audit-log-gc
|
||||
:audit-log-logger
|
||||
:auto-file-snapshot
|
||||
;; enables the `/api/doc` endpoint that lists all the rpc methods available.
|
||||
:backend-api-doc
|
||||
@@ -134,6 +135,8 @@
|
||||
:subscriptions
|
||||
:subscriptions-old
|
||||
:inspect-styles
|
||||
;; Enable performance logs in devconsole (disabled by default)
|
||||
:perf-logs
|
||||
|
||||
;; Security layer middleware that filters request by fetch
|
||||
;; metadata headers
|
||||
|
||||
@@ -64,8 +64,33 @@
|
||||
java.time.temporal.TemporalAmount
|
||||
java.time.temporal.TemporalUnit)))
|
||||
|
||||
(declare inst)
|
||||
|
||||
#?(:clj (def ^:dynamic *clock* (Clock/systemDefaultZone)))
|
||||
|
||||
#?(:clj
|
||||
(defn clock?
|
||||
[o]
|
||||
(instance? Clock o)))
|
||||
|
||||
#?(:clj
|
||||
(defn get-system-clock
|
||||
[]
|
||||
(Clock/systemDefaultZone)))
|
||||
|
||||
#?(:clj
|
||||
(defn offset-clock
|
||||
[offset]
|
||||
(Clock/offset ^Clock (Clock/systemDefaultZone) ^Duration offset)))
|
||||
|
||||
#?(:clj
|
||||
(defn fixed-clock
|
||||
[instant]
|
||||
(Clock/fixed ^Instant (inst instant)
|
||||
^ZoneId (ZoneId/of "Z"))))
|
||||
|
||||
|
||||
|
||||
(defn now
|
||||
[]
|
||||
#?(:clj (Instant/now *clock*)
|
||||
|
||||
@@ -474,8 +474,10 @@
|
||||
:height #{:sizing :dimensions}
|
||||
:max-width #{:sizing :dimensions}
|
||||
:max-height #{:sizing :dimensions}
|
||||
:x #{:spacing :dimensions}
|
||||
:y #{:spacing :dimensions}
|
||||
:min-width #{:sizing :dimensions}
|
||||
:min-height #{:sizing :dimensions}
|
||||
:x #{:dimensions}
|
||||
:y #{:dimensions}
|
||||
:rotation #{:number :rotation}
|
||||
:border-radius #{:border-radius :dimensions}
|
||||
:row-gap #{:spacing :dimensions}
|
||||
@@ -488,6 +490,7 @@
|
||||
:sided-margins #{:spacing :dimensions}
|
||||
:line-height #{:line-height :number}
|
||||
:opacity #{:opacity}
|
||||
:stroke-width #{:stroke-width :dimensions}
|
||||
:font-size #{:font-size}
|
||||
:letter-spacing #{:letter-spacing}
|
||||
:fill #{:color}
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
{:path path
|
||||
:mtype (mime/get type)
|
||||
:name name
|
||||
:filename (str/concat name (mime/get-extension type))
|
||||
:filename (str/concat (str/slug name) (mime/get-extension type))
|
||||
:id task-id}))
|
||||
|
||||
(defn create-zip
|
||||
|
||||
@@ -50,16 +50,16 @@
|
||||
"@penpot/plugins-runtime": "1.4.2",
|
||||
"@penpot/svgo": "penpot/svgo#v3.2",
|
||||
"@penpot/text-editor": "workspace:./text-editor",
|
||||
"@playwright/test": "1.57.0",
|
||||
"@playwright/test": "1.58.0",
|
||||
"@storybook/addon-docs": "10.1.11",
|
||||
"@storybook/addon-themes": "10.1.11",
|
||||
"@storybook/addon-vitest": "10.1.11",
|
||||
"@storybook/react-vite": "10.1.11",
|
||||
"@tokens-studio/sd-transforms": "1.2.11",
|
||||
"@types/node": "^25.0.3",
|
||||
"@vitest/browser": "4.0.16",
|
||||
"@vitest/browser-playwright": "^4.0.16",
|
||||
"@vitest/coverage-v8": "4.0.16",
|
||||
"@vitest/browser": "4.0.18",
|
||||
"@vitest/browser-playwright": "^4.0.18",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
"@zip.js/zip.js": "2.8.11",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"compression": "^1.8.1",
|
||||
@@ -84,7 +84,7 @@
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opentype.js": "^1.3.4",
|
||||
"p-limit": "^6.2.0",
|
||||
"playwright": "1.57.0",
|
||||
"playwright": "1.58.0",
|
||||
"postcss": "^8.5.4",
|
||||
"postcss-clean": "^1.2.2",
|
||||
"postcss-modules": "^6.0.1",
|
||||
@@ -109,8 +109,8 @@
|
||||
"tinycolor2": "^1.6.0",
|
||||
"typescript": "^5.9.2",
|
||||
"ua-parser-js": "2.0.7",
|
||||
"vite": "^7.3.0",
|
||||
"vitest": "^4.0.16",
|
||||
"vite": "^7.3.1",
|
||||
"vitest": "^4.0.18",
|
||||
"wait-on": "^9.0.3",
|
||||
"wasm-pack": "^0.13.1",
|
||||
"watcher": "^2.3.1",
|
||||
|
||||
@@ -64,6 +64,11 @@ export default defineConfig({
|
||||
...devices["Desktop Chrome"],
|
||||
viewport: { width: 1920, height: 1080 }, // Add custom viewport size
|
||||
deviceScaleFactor: 2,
|
||||
launchOptions: {
|
||||
args: [
|
||||
'--enable-gpu',
|
||||
],
|
||||
}
|
||||
},
|
||||
testDir: "./playwright/ui/render-wasm-specs",
|
||||
snapshotPathTemplate: "{testDir}/{testFilePath}-snapshots/{arg}.png",
|
||||
@@ -80,7 +85,7 @@ export default defineConfig({
|
||||
/* Run your local dev server before starting the tests */
|
||||
webServer: {
|
||||
timeout: 2 * 60 * 1000,
|
||||
command: "caddy file-server --root resources/public/ --listen :3000",
|
||||
command: "node ./scripts/e2e-server.js",
|
||||
url: "http://localhost:3000",
|
||||
reuseExistingServer: !process.env.CI,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"~:features": {
|
||||
"~#set": [
|
||||
"fdata/path-data",
|
||||
"plugins/runtime",
|
||||
"design-tokens/v1",
|
||||
"variants/v1",
|
||||
"layout/grid",
|
||||
"styles/v2",
|
||||
"fdata/objects-map",
|
||||
"components/v2",
|
||||
"fdata/shape-data-type"
|
||||
]
|
||||
},
|
||||
"~:team-id": "~u55ffbd0f-2d3f-8023-8007-6b89af7ca1aa",
|
||||
"~: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": "Nuevo Archivo 2",
|
||||
"~:revn": 18,
|
||||
"~:modified-at": "~m1768994999265",
|
||||
"~:vern": 0,
|
||||
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314bfe8",
|
||||
"~: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": "~u55ffbd0f-2d3f-8023-8007-6b89af7cd5eb",
|
||||
"~:created-at": "~m1768562403410",
|
||||
"~:backend": "legacy-db",
|
||||
"~:data": {
|
||||
"~:pages": [
|
||||
"~u8bb92298-e24e-805c-8007-6ce64314cfc5"
|
||||
],
|
||||
"~:pages-index": {
|
||||
"~u8bb92298-e24e-805c-8007-6ce64314cfc5": {
|
||||
"~: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]],\"~: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,\"^H\",0.01,\"~:flip-y\",null,\"~:shapes\",[\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~u8506e3f3-e05b-807c-8007-6ceac380abc1\"]]]",
|
||||
"~u6c65a5dc-fb40-8072-8007-6ce644905054": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~:hide-fill-on-export\",false,\"~:layout-gap-type\",\"~:multiple\",\"~:layout-padding\",[\"^ \",\"~:p1\",0,\"~:p2\",0,\"~:p3\",0,\"~:p4\",0],\"~: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\",\"~:grow-type\",\"~:fixed\",\"~:layout\",\"~:flex\",\"~:hide-in-viewer\",false,\"~:name\",\"Board\",\"~:layout-align-items\",\"~:start\",\"~:width\",389,\"~:layout-padding-type\",\"~:simple\",\"~:type\",\"~:frame\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",303,\"~:y\",159]],[\"^L\",[\"^ \",\"~:x\",692,\"~:y\",159]],[\"^L\",[\"^ \",\"~:x\",692,\"~:y\",599]],[\"^L\",[\"^ \",\"~:x\",303,\"~:y\",599]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:layout-gap\",[\"^ \",\"~:row-gap\",0,\"~:column-gap\",0],\"~:transform-inverse\",[\"^:\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:layout-justify-content\",\"^E\",\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:layout-flex-dir\",\"~:row\",\"~:layout-align-content\",\"~:stretch\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",303,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",303,\"~:y\",159,\"^F\",389,\"~:height\",440,\"~:x1\",303,\"~:y1\",159,\"~:x2\",692,\"~:y2\",599]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#FFFFFF\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^16\",440,\"~:flip-y\",null,\"~:shapes\",[\"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9\",\"~u6c65a5dc-fb40-8072-8007-6ce65472f217\",\"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62\"]]]",
|
||||
"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~: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\",95,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",303,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",259]],[\"^<\",[\"^ \",\"~:x\",303,\"~:y\",259]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce6535e8d62\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",303,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",303,\"~:y\",159,\"^8\",95,\"~:height\",100,\"~:x1\",303,\"~:y1\",159,\"~:x2\",398,\"~:y2\",259]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",100,\"~:flip-y\",null]]",
|
||||
"~u6c65a5dc-fb40-8072-8007-6ce65472f217": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~: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\",140.99999999999994,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",398,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",296]],[\"^<\",[\"^ \",\"~:x\",398,\"~:y\",296]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce65472f217\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",398.00000000000006,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",398.00000000000006,\"~:y\",159,\"^8\",140.99999999999994,\"~:height\",137,\"~:x1\",398.00000000000006,\"~:y1\",159,\"~:x2\",539,\"~:y2\",296]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",137,\"~:flip-y\",null]]",
|
||||
"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9": "[\"~#shape\",[\"^ \",\"~:y\",159,\"~: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\",82,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",539,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",621,\"~:y\",159]],[\"^<\",[\"^ \",\"~:x\",621,\"~:y\",259]],[\"^<\",[\"^ \",\"~:x\",539,\"~:y\",259]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u6c65a5dc-fb40-8072-8007-6ce655b5dbe9\",\"~:parent-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:frame-id\",\"~u6c65a5dc-fb40-8072-8007-6ce644905054\",\"~:strokes\",[],\"~:x\",539,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",539,\"~:y\",159,\"^8\",82,\"~:height\",100,\"~:x1\",539,\"~:y1\",159,\"~:x2\",621,\"~:y2\",259]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",100,\"~:flip-y\",null]]",
|
||||
"~u8506e3f3-e05b-807c-8007-6ceac380abc1": "[\"~#shape\",[\"^ \",\"~:y\",405,\"~: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\",59,\"~:type\",\"~:rect\",\"~:points\",[[\"~#point\",[\"^ \",\"~:x\",817,\"~:y\",405]],[\"^<\",[\"^ \",\"~:x\",876,\"~:y\",405]],[\"^<\",[\"^ \",\"~:x\",876,\"~:y\",472]],[\"^<\",[\"^ \",\"~:x\",817,\"~:y\",472]]],\"~:r2\",0,\"~:proportion-lock\",false,\"~:transform-inverse\",[\"^2\",[\"^ \",\"~:a\",1.0,\"~:b\",0.0,\"~:c\",0.0,\"~:d\",1.0,\"~:e\",0.0,\"~:f\",0.0]],\"~:r3\",0,\"~:r1\",0,\"~:id\",\"~u8506e3f3-e05b-807c-8007-6ceac380abc1\",\"~:parent-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:frame-id\",\"~u00000000-0000-0000-0000-000000000000\",\"~:strokes\",[],\"~:x\",817,\"~:proportion\",1,\"~:r4\",0,\"~:selrect\",[\"~#rect\",[\"^ \",\"~:x\",817,\"~:y\",405,\"^8\",59,\"~:height\",67,\"~:x1\",817,\"~:y1\",405,\"~:x2\",876,\"~:y2\",472]],\"~:fills\",[[\"^ \",\"~:fill-color\",\"#B1B2B5\",\"~:fill-opacity\",1]],\"~:flip-x\",null,\"^J\",67,\"~:flip-y\",null]]"
|
||||
}
|
||||
},
|
||||
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314cfc5",
|
||||
"~:name": "Page 1"
|
||||
}
|
||||
},
|
||||
"~:id": "~u8bb92298-e24e-805c-8007-6ce64314bfe8",
|
||||
"~:options": {
|
||||
"~:components-v2": true,
|
||||
"~:base-font-size": "16px"
|
||||
},
|
||||
"~:tokens-lib": {
|
||||
"~#penpot/tokens-lib": {
|
||||
"~:sets": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"S-Global",
|
||||
{
|
||||
"~#penpot/token-set": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce67f8f2592",
|
||||
"~:name": "Global",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768994999268",
|
||||
"~:tokens": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"dim.xs",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce67f8f2591",
|
||||
"~:name": "dim.xs",
|
||||
"~:type": "~:dimensions",
|
||||
"~:value": "20",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562465340"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"dim.md",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce68a2f3c3f",
|
||||
"~:name": "dim.md",
|
||||
"~:type": "~:dimensions",
|
||||
"~:value": "50",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562476220"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"dim.xl",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce694f47726",
|
||||
"~:name": "dim.xl",
|
||||
"~:type": "~:dimensions",
|
||||
"~:value": "100",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562487249"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"sz.sm",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6d6e519b1",
|
||||
"~:name": "sz.sm",
|
||||
"~:type": "~:sizing",
|
||||
"~:value": "200",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562554772"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"sz.xl",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6e4165307",
|
||||
"~:name": "sz.xl",
|
||||
"~:type": "~:sizing",
|
||||
"~:value": "500",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562568281"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"sp.mid",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce6f5fb2867",
|
||||
"~:name": "sp.mid",
|
||||
"~:type": "~:spacing",
|
||||
"~:value": "50",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562586604"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"sp.l",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u7c1a66f7-5186-8060-8007-6ce71009cd91",
|
||||
"~:name": "sp.l",
|
||||
"~:type": "~:spacing",
|
||||
"~:value": "{dim.xl}",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768562613287"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"test",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~uc49f25e6-1298-8004-8007-709f660875be",
|
||||
"~:name": "test",
|
||||
"~:type": "~:dimensions",
|
||||
"~:value": "20",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768812262433"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"width-big",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u3b3e4320-53fb-8096-8007-73585edb4dd6",
|
||||
"~:name": "width-big",
|
||||
"~:type": "~:stroke-width",
|
||||
"~:value": "20",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768994969453"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"width-small",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u3b3e4320-53fb-8096-8007-73586a5ec516",
|
||||
"~:name": "width-small",
|
||||
"~:type": "~:stroke-width",
|
||||
"~:value": "5",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768994981243"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"red",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u3b3e4320-53fb-8096-8007-735871dccede",
|
||||
"~:name": "red",
|
||||
"~:type": "~:color",
|
||||
"~:value": "red",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768994988915"
|
||||
}
|
||||
}
|
||||
],
|
||||
[
|
||||
"green",
|
||||
{
|
||||
"~#penpot/token": {
|
||||
"~:id": "~u3b3e4320-53fb-8096-8007-735879045aa2",
|
||||
"~:name": "green",
|
||||
"~:type": "~:color",
|
||||
"~:value": "green",
|
||||
"~:description": "",
|
||||
"~:modified-at": "~m1768994996241"
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"~:themes": {
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"",
|
||||
{
|
||||
"~#ordered-map": [
|
||||
[
|
||||
"__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||
{
|
||||
"~#penpot/token-theme": {
|
||||
"~:id": "~u00000000-0000-0000-0000-000000000000",
|
||||
"~:name": "__PENPOT__HIDDEN__TOKEN__THEME__",
|
||||
"~:group": "",
|
||||
"~:description": "",
|
||||
"~:is-source": false,
|
||||
"~:external-id": "",
|
||||
"~:modified-at": "~m1768562468391",
|
||||
"~:sets": {
|
||||
"~#set": [
|
||||
"Global"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"~:active-themes": {
|
||||
"~#set": [
|
||||
"/__PENPOT__HIDDEN__TOKEN__THEME__"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,12 +149,14 @@ test.describe("Tokens: Apply token", () => {
|
||||
await detachButton.click();
|
||||
|
||||
// Open dropdown from input
|
||||
const dropdownBtn = layerMenuSection.getByLabel('Open token list');
|
||||
const dropdownBtn = layerMenuSection.getByLabel("Open token list");
|
||||
await expect(dropdownBtn).toBeVisible();
|
||||
await dropdownBtn.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const opacityLowOption = layerMenuSection.getByRole('option', { name: 'opacity.low' });
|
||||
const opacityLowOption = layerMenuSection.getByRole("option", {
|
||||
name: "opacity.low",
|
||||
});
|
||||
await expect(opacityLowOption).toBeVisible();
|
||||
await opacityLowOption.click();
|
||||
|
||||
@@ -482,4 +484,362 @@ test.describe("Tokens: Apply token", () => {
|
||||
await expect(shadowSection).toHaveCount(2);
|
||||
});
|
||||
});
|
||||
|
||||
test("User applies dimension token to a shape on width and height", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
|
||||
|
||||
// Apply token to width and height token from token panel
|
||||
await tokensSidebar.getByRole("button", { name: "dimension.sm" }).click();
|
||||
|
||||
// Check if measures sections is visible on right sidebar
|
||||
const measuresSection = page.getByRole("region", {
|
||||
name: "shape-measures-section",
|
||||
});
|
||||
await expect(measuresSection).toBeVisible();
|
||||
|
||||
// Check if token pill is visible on design tab on right sidebar
|
||||
const dimensionSMTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.sm",
|
||||
});
|
||||
await expect(dimensionSMTokenPill).toHaveCount(2);
|
||||
await dimensionSMTokenPill.nth(1).click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(dimensionSMTokenPill).toHaveCount(1);
|
||||
const dimensionXLTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionXLTokenPill).toBeVisible();
|
||||
|
||||
// Detach token from design tab on right sidebar
|
||||
const detachButton = measuresSection.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.nth(1).click();
|
||||
await expect(dimensionXLTokenPill).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies dimension token to a shape on x position", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
|
||||
|
||||
// Apply token to width and height token from token panel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "dimension.sm" })
|
||||
.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("AxisX").click();
|
||||
|
||||
// Check if measures sections is visible on right sidebar
|
||||
const measuresSection = page.getByRole("region", {
|
||||
name: "shape-measures-section",
|
||||
});
|
||||
await expect(measuresSection).toBeVisible();
|
||||
|
||||
// Check if token pill is visible on design tab on right sidebar
|
||||
const dimensionSMTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.sm",
|
||||
});
|
||||
await expect(dimensionSMTokenPill).toBeVisible();
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(dimensionSMTokenPill).not.toBeVisible();
|
||||
const dimensionXLTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionXLTokenPill).toBeVisible();
|
||||
|
||||
// Detach token from design tab on right sidebar
|
||||
const detachButton = measuresSection.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.nth(0).click();
|
||||
await expect(dimensionXLTokenPill).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies dimension token to a shape on y position", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
|
||||
|
||||
// Apply token to width and height token from token panel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "dimension.sm" })
|
||||
.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Y").click();
|
||||
|
||||
// Check if measures sections is visible on right sidebar
|
||||
const measuresSection = page.getByRole("region", {
|
||||
name: "shape-measures-section",
|
||||
});
|
||||
await expect(measuresSection).toBeVisible();
|
||||
|
||||
// Check if token pill is visible on design tab on right sidebar
|
||||
const dimensionSMTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.sm",
|
||||
});
|
||||
await expect(dimensionSMTokenPill).toBeVisible();
|
||||
await dimensionSMTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(dimensionSMTokenPill).not.toBeVisible();
|
||||
const dimensionXLTokenPill = measuresSection.getByRole("button", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionXLTokenPill).toBeVisible();
|
||||
|
||||
// Detach token from design tab on right sidebar
|
||||
const detachButton = measuresSection.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.nth(0).click();
|
||||
await expect(dimensionXLTokenPill).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies dimension token to a shape border-radius", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
|
||||
// Unfolds dimensions on token panel
|
||||
await page.getByRole("tab", { name: "Layers" }).click();
|
||||
|
||||
await workspacePage.layers.getByTestId("layer-row").nth(2).click();
|
||||
|
||||
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||
await tokensTabButton.click();
|
||||
|
||||
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.xs");
|
||||
|
||||
// Apply token to width and height token from token panel
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "dimension.xs" })
|
||||
.click({ button: "right" });
|
||||
await tokenContextMenuForToken.getByText("Border radius").hover();
|
||||
await tokenContextMenuForToken.getByText("RadiusAll").click();
|
||||
|
||||
// Check if border radius sections is visible on right sidebar
|
||||
const borderRadiusSection = page.getByRole("region", {
|
||||
name: "border-radius-section",
|
||||
});
|
||||
await expect(borderRadiusSection).toBeVisible();
|
||||
|
||||
// Check if token pill is visible on design tab on right sidebar
|
||||
const dimensionXSTokenPill = borderRadiusSection.getByRole("button", {
|
||||
name: "dimension.xs",
|
||||
});
|
||||
await expect(dimensionXSTokenPill).toBeVisible();
|
||||
await dimensionXSTokenPill.click();
|
||||
|
||||
// Change token from dropdown
|
||||
const dimensionTokenOptionXl =
|
||||
borderRadiusSection.getByLabel("dimension.xl");
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(dimensionXSTokenPill).not.toBeVisible();
|
||||
const dimensionXLTokenPill = borderRadiusSection.getByRole("button", {
|
||||
name: "dimension.xl",
|
||||
});
|
||||
await expect(dimensionXLTokenPill).toBeVisible();
|
||||
|
||||
// Detach token from design tab on right sidebar
|
||||
const detachButton = borderRadiusSection.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.nth(0).click();
|
||||
await expect(dimensionXLTokenPill).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies stroke width token to a shape", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
await workspace.mockConfigFlags(["enable-feature-token-input"]);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("workspace/get-file-layout-stroke-token-json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
// Select shape apply stroke
|
||||
await workspace.layers.getByTestId("layer-row").nth(0).click();
|
||||
const rightSidebar = page.getByTestId("right-sidebar");
|
||||
await expect(rightSidebar).toBeVisible();
|
||||
await rightSidebar.getByTestId("add-stroke").click();
|
||||
|
||||
// Apply stroke width token from token panel
|
||||
const tokensTab = page.getByRole("tab", { name: "Tokens" });
|
||||
await expect(tokensTab).toBeVisible();
|
||||
await tokensTab.click();
|
||||
await page.getByRole("button", { name: "Stroke Width 2" }).click();
|
||||
const tokensSidebar = workspace.tokensSidebar;
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "width-big" }),
|
||||
).toBeVisible();
|
||||
await tokensSidebar.getByRole("button", { name: "width-big" }).click();
|
||||
|
||||
// Check if token pill is visible on right sidebar
|
||||
const strokeSectionSidebar = rightSidebar.getByRole("region", {
|
||||
name: "stroke-section",
|
||||
});
|
||||
await expect(strokeSectionSidebar).toBeVisible();
|
||||
const firstStrokeRow = strokeSectionSidebar.getByLabel("stroke-row-0");
|
||||
await expect(firstStrokeRow).toBeVisible();
|
||||
const StrokeWidthPill = firstStrokeRow.getByRole("button", {
|
||||
name: "width-big",
|
||||
});
|
||||
await expect(StrokeWidthPill).toBeVisible();
|
||||
|
||||
// Detach token from right sidebar and apply another from dropdown
|
||||
const detachButton = firstStrokeRow.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.click();
|
||||
await expect(StrokeWidthPill).not.toBeVisible();
|
||||
|
||||
const tokenDropdown = firstStrokeRow.getByRole("button", {
|
||||
name: "Open token list",
|
||||
});
|
||||
await tokenDropdown.click();
|
||||
|
||||
const widthOptionSmall = firstStrokeRow.getByLabel("width-small");
|
||||
await expect(widthOptionSmall).toBeVisible();
|
||||
await widthOptionSmall.click();
|
||||
const StrokeWidthPillSmall = firstStrokeRow.getByRole("button", {
|
||||
name: "width-small",
|
||||
});
|
||||
await expect(StrokeWidthPillSmall).toBeVisible();
|
||||
});
|
||||
|
||||
test("User applies margin token to a shape", async ({ page }) => {
|
||||
const workspace = new WorkspacePage(page, {
|
||||
textEditor: true,
|
||||
});
|
||||
// Set up
|
||||
await workspace.mockConfigFlags(["enable-feature-token-input"]);
|
||||
await workspace.setupEmptyFile();
|
||||
await workspace.mockGetFile("workspace/get-file-layout-stroke-token-json");
|
||||
await workspace.goToWorkspace();
|
||||
|
||||
// Select shape apply stroke
|
||||
await workspace.layers
|
||||
.getByTestId("layer-row")
|
||||
.nth(1)
|
||||
.getByRole("button", { name: "Toggle layer" })
|
||||
.click();
|
||||
|
||||
await workspace.layers.getByTestId("layer-row").nth(2).click();
|
||||
|
||||
const rightSidebar = page.getByTestId("right-sidebar");
|
||||
await expect(rightSidebar).toBeVisible();
|
||||
await rightSidebar.getByTestId("add-stroke").click();
|
||||
|
||||
// Apply margin token from token panel
|
||||
const tokensTab = page.getByRole("tab", { name: "Tokens" });
|
||||
await expect(tokensTab).toBeVisible();
|
||||
await tokensTab.click();
|
||||
await page.getByRole("button", { name: "Dimensions 4" }).click();
|
||||
await page.getByRole("button", { name: "dim", exact: true }).click();
|
||||
const tokensSidebar = workspace.tokensSidebar;
|
||||
await expect(
|
||||
tokensSidebar.getByRole("button", { name: "dim.md" }),
|
||||
).toBeVisible();
|
||||
await tokensSidebar
|
||||
.getByRole("button", { name: "dim.md" })
|
||||
.click({ button: "right" });
|
||||
await page
|
||||
.getByTestId("tokens-context-menu-for-token")
|
||||
.getByText("Spacing")
|
||||
.hover();
|
||||
await page
|
||||
.getByTestId("tokens-context-menu-for-token")
|
||||
.getByText("Horizontal")
|
||||
.click();
|
||||
|
||||
// Check if token pill is visible on right sidebar
|
||||
const layoutItemSectionSidebar = rightSidebar.getByRole("region", {
|
||||
name: "layout item menu",
|
||||
});
|
||||
await expect(layoutItemSectionSidebar).toBeVisible();
|
||||
const marginPillMd = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "dim.md",
|
||||
});
|
||||
await expect(marginPillMd).toBeVisible();
|
||||
|
||||
await marginPillMd.click();
|
||||
const dimensionTokenOptionXl = page.getByRole("option", { name: "dim.xl" });
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
const marginPillXL = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "dim.xl",
|
||||
});
|
||||
await expect(marginPillXL).toBeVisible();
|
||||
|
||||
// Detach token from right sidebar and apply another from dropdown
|
||||
const detachButton = layoutItemSectionSidebar.getByRole("button", {
|
||||
name: "Detach token",
|
||||
});
|
||||
await detachButton.click();
|
||||
await expect(marginPillXL).not.toBeVisible();
|
||||
const horizontalMarginInput = layoutItemSectionSidebar.getByText('Horizontal marginOpen token');
|
||||
await expect(horizontalMarginInput).toBeVisible();
|
||||
|
||||
const tokenDropdown = horizontalMarginInput.getByRole('button', { name: 'Open token list' });
|
||||
await tokenDropdown.click();
|
||||
|
||||
await expect(dimensionTokenOptionXl).toBeVisible();
|
||||
await dimensionTokenOptionXl.click();
|
||||
|
||||
await expect(marginPillXL).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
530
frontend/pnpm-lock.yaml
generated
530
frontend/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 138 KiB |
@@ -13,7 +13,7 @@
|
||||
$weight: unquote("normal"),
|
||||
$style: string.unquote("normal")
|
||||
) {
|
||||
$filepath: "/fonts/" + $file;
|
||||
$filepath: "../fonts/" + $file;
|
||||
|
||||
@font-face {
|
||||
font-family: "#{$style-name}";
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
@mixin font-face-variable($style-name, $file, $unicode-range) {
|
||||
$filepath: "/fonts/" + $file;
|
||||
$filepath: "../fonts/" + $file;
|
||||
|
||||
@font-face {
|
||||
font-family: "#{$style-name}";
|
||||
|
||||
20
frontend/scripts/e2e-server.js
Normal file
20
frontend/scripts/e2e-server.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import express from "express";
|
||||
import compression from "compression";
|
||||
|
||||
import { fileURLToPath } from "url";
|
||||
import path from "path";
|
||||
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
app.use(compression());
|
||||
|
||||
const staticPath = path.join(
|
||||
fileURLToPath(import.meta.url),
|
||||
"../../resources/public",
|
||||
);
|
||||
app.use(express.static(staticPath));
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Listening at 0.0.0.0:${port}`);
|
||||
});
|
||||
@@ -111,12 +111,9 @@
|
||||
|
||||
(defn- normalize-uri
|
||||
[uri-str]
|
||||
(let [uri (u/uri uri-str)]
|
||||
;; Ensure that the path always ends with "/"; this ensures that
|
||||
;; all path join operations works as expected.
|
||||
(cond-> uri
|
||||
(not (str/ends-with? (:path uri) "/"))
|
||||
(update :path #(str % "/")))))
|
||||
;; Ensure that the path always ends with "/"; this ensures that
|
||||
;; all path join operations works as expected.
|
||||
(u/ensure-path-slash uri-str))
|
||||
|
||||
(def public-uri
|
||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||
|
||||
@@ -476,23 +476,24 @@
|
||||
(when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*))
|
||||
(let [observer (js/PerformanceObserver.
|
||||
(fn [list _]
|
||||
(doseq [entry (.getEntries list)]
|
||||
(let [dur (.-duration entry)
|
||||
start (.-startTime entry)
|
||||
attrib (.-attribution entry)
|
||||
attrib-count (when attrib (.-length attrib))
|
||||
first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0))
|
||||
attrib-name (when first-attrib (.-name first-attrib))
|
||||
attrib-ctype (when first-attrib (.-containerType first-attrib))
|
||||
attrib-cid (when first-attrib (.-containerId first-attrib))
|
||||
attrib-csrc (when first-attrib (.-containerSrc first-attrib))]
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(doseq [entry (.getEntries list)]
|
||||
(let [dur (.-duration entry)
|
||||
start (.-startTime entry)
|
||||
attrib (.-attribution entry)
|
||||
attrib-count (when attrib (.-length attrib))
|
||||
first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0))
|
||||
attrib-name (when first-attrib (.-name first-attrib))
|
||||
attrib-ctype (when first-attrib (.-containerType first-attrib))
|
||||
attrib-cid (when first-attrib (.-containerId first-attrib))
|
||||
attrib-csrc (when first-attrib (.-containerSrc first-attrib))]
|
||||
|
||||
(.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms"
|
||||
(when first-attrib
|
||||
(str " attrib:name=" attrib-name
|
||||
" ctype=" attrib-ctype
|
||||
" cid=" attrib-cid
|
||||
" csrc=" attrib-csrc))))))))]
|
||||
(.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms"
|
||||
(when first-attrib
|
||||
(str " attrib:name=" attrib-name
|
||||
" ctype=" attrib-ctype
|
||||
" cid=" attrib-cid
|
||||
" csrc=" attrib-csrc)))))))))]
|
||||
(.observe observer #js{:entryTypes #js["longtask"]})
|
||||
(reset! longtask-observer* observer))))
|
||||
|
||||
@@ -505,28 +506,30 @@
|
||||
(let [last (atom (.now js/performance))
|
||||
id (js/setInterval
|
||||
(fn []
|
||||
(let [now (.now js/performance)
|
||||
expected (+ @last interval-ms)
|
||||
drift (- now expected)
|
||||
current-op @current-op*
|
||||
measures (.getEntriesByType js/performance "measure")
|
||||
mlen (.-length measures)
|
||||
last-measure (when (> mlen 0) (aget measures (dec mlen)))
|
||||
meas-name (when last-measure (.-name last-measure))
|
||||
meas-detail (when last-measure (.-detail last-measure))
|
||||
meas-count (when meas-detail (unchecked-get meas-detail "count"))]
|
||||
(reset! last now)
|
||||
(when (> drift threshold-ms)
|
||||
(.warn js/console
|
||||
(str "[perf] event loop stall: " (Math/round drift) "ms"
|
||||
(when current-op (str " op=" current-op))
|
||||
(when meas-name (str " last=" meas-name))
|
||||
(when meas-count (str " count=" meas-count)))))))
|
||||
(when (contains? cf/flags :perf-logs)
|
||||
(let [now (.now js/performance)
|
||||
expected (+ @last interval-ms)
|
||||
drift (- now expected)
|
||||
current-op @current-op*
|
||||
measures (.getEntriesByType js/performance "measure")
|
||||
mlen (.-length measures)
|
||||
last-measure (when (> mlen 0) (aget measures (dec mlen)))
|
||||
meas-name (when last-measure (.-name last-measure))
|
||||
meas-detail (when last-measure (.-detail last-measure))
|
||||
meas-count (when meas-detail (unchecked-get meas-detail "count"))]
|
||||
(reset! last now)
|
||||
(when (> drift threshold-ms)
|
||||
(.warn js/console
|
||||
(str "[perf] event loop stall: " (Math/round drift) "ms"
|
||||
(when current-op (str " op=" current-op))
|
||||
(when meas-name (str " last=" meas-name))
|
||||
(when meas-count (str " count=" meas-count))))))))
|
||||
interval-ms)]
|
||||
(reset! stall-timer* id))))
|
||||
|
||||
(defn init!
|
||||
"Install perf observers in dev builds. Safe to call multiple times."
|
||||
"Install perf observers in dev builds. Safe to call multiple times.
|
||||
Perf logs are disabled by default. Enable them with the :perf-logs flag in config."
|
||||
[]
|
||||
(when ^boolean js/goog.DEBUG
|
||||
(install-long-task-observer!)
|
||||
|
||||
@@ -24,6 +24,20 @@
|
||||
[cuerdas.core :as str]
|
||||
[potok.v2.core :as ptk]))
|
||||
|
||||
(def ^:const default-chunk-size
|
||||
(* 1024 1024 4)) ;; 4MiB
|
||||
|
||||
(defn- chunk-array
|
||||
[data chunk-size]
|
||||
(let [total-size (alength data)]
|
||||
(loop [offset 0
|
||||
chunks []]
|
||||
(if (< offset total-size)
|
||||
(let [end (min (+ offset chunk-size) total-size)
|
||||
chunk (.subarray ^js data offset end)]
|
||||
(recur end (conj chunks chunk)))
|
||||
chunks))))
|
||||
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
;; General purpose events & IMPL
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
@@ -116,9 +130,9 @@
|
||||
(not= hhea-descender win-descent)
|
||||
(and f-selection (or
|
||||
(not= hhea-ascender os2-ascent)
|
||||
(not= hhea-descender os2-descent))))]
|
||||
|
||||
{:content {:data (js/Uint8Array. data)
|
||||
(not= hhea-descender os2-descent))))
|
||||
data (js/Uint8Array. data)]
|
||||
{:content {:data (chunk-array data default-chunk-size)
|
||||
:name name
|
||||
:type type}
|
||||
:font-family (or family "")
|
||||
|
||||
@@ -70,20 +70,22 @@
|
||||
(= (-> content last :command) :move-to))
|
||||
(into [] (take (dec (count content)) content))
|
||||
content)]
|
||||
(-> state
|
||||
(st/set-content content))))
|
||||
(st/set-content state content)))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [page-id (:current-page-id state)
|
||||
objects (dsh/lookup-page-objects state page-id)
|
||||
id (dm/get-in state [:workspace-local :edition])
|
||||
old-content (dm/get-in state [:workspace-local :edit-path id :old-content])
|
||||
shape (st/get-path state)]
|
||||
local (get state :workspace-local)
|
||||
id (get local :edition)
|
||||
objects (dsh/lookup-page-objects state page-id)]
|
||||
|
||||
(if (and (some? old-content) (some? (:id shape)))
|
||||
(let [changes (generate-path-changes it objects page-id shape old-content (:content shape))]
|
||||
(rx/of (dch/commit-changes changes)))
|
||||
(rx/empty)))))))
|
||||
;; NOTE: we proceed only if the shape is present on the
|
||||
;; objects, if shape is a ephimeral drawing shape, we should
|
||||
;; do nothing
|
||||
(when-let [shape (get objects id)]
|
||||
(when-let [old-content (dm/get-in local [:edit-path id :old-content])]
|
||||
(let [new-content (get shape :content)
|
||||
changes (generate-path-changes it objects page-id shape old-content new-content)]
|
||||
(rx/of (dch/commit-changes changes))))))))))
|
||||
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.geom.point :as gpt]
|
||||
[app.common.types.path :as path]
|
||||
[app.common.types.path.helpers :as path.helpers]
|
||||
@@ -289,34 +288,34 @@
|
||||
|
||||
(declare stop-path-edit)
|
||||
|
||||
|
||||
(defn start-path-edit
|
||||
[id]
|
||||
(ptk/reify ::start-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
edit-path (dm/get-in state [:workspace-local :edit-path id])
|
||||
content (st/get-path state :content)
|
||||
state (cond-> state
|
||||
(cfh/path-shape? objects id)
|
||||
(st/set-content (path/close-subpaths content)))]
|
||||
shape (get objects id)]
|
||||
|
||||
(cond-> state
|
||||
(or (not edit-path)
|
||||
(= :draw (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id] {:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})
|
||||
(and (some? edit-path)
|
||||
(= :move (:edit-mode edit-path)))
|
||||
(assoc-in [:workspace-local :edit-path id :edit-mode] :draw))))
|
||||
(-> state
|
||||
(st/set-content (path/close-subpaths (:content shape)))
|
||||
(update-in [:workspace-local :edit-path id]
|
||||
(fn [state]
|
||||
(let [state (if state
|
||||
(if (= :move (:edit-mode state))
|
||||
(assoc state :edit-mode :draw)
|
||||
state)
|
||||
{:edit-mode :move
|
||||
:selected #{}
|
||||
:snap-toggled false})]
|
||||
(assoc state :old-content (:content shape))))))))
|
||||
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ stream]
|
||||
(let [stopper (->> stream
|
||||
(rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))))]
|
||||
(let [stopper (rx/filter #(let [type (ptk/type %)]
|
||||
(= type ::dwe/clear-edition-mode)
|
||||
(= type ::start-path-edit))
|
||||
stream)]
|
||||
(rx/concat
|
||||
(rx/of (undo/start-path-undo))
|
||||
(->> stream
|
||||
@@ -325,7 +324,8 @@
|
||||
(rx/map #(stop-path-edit id))
|
||||
(rx/take-until stopper)))))))
|
||||
|
||||
(defn stop-path-edit [id]
|
||||
(defn stop-path-edit
|
||||
[id]
|
||||
(ptk/reify ::stop-path-edit
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
@@ -335,13 +335,12 @@
|
||||
(watch [_ _ _]
|
||||
(rx/of (ptk/data-event :layout/update {:ids [id]})))))
|
||||
|
||||
(defn split-segments
|
||||
[{:keys [from-p to-p t]}]
|
||||
(defn- split-segments
|
||||
[id {:keys [from-p to-p t]}]
|
||||
(ptk/reify ::split-segments
|
||||
ptk/UpdateEvent
|
||||
(update [_ state]
|
||||
(let [id (st/get-path-id state)
|
||||
content (st/get-path state :content)]
|
||||
(let [content (st/get-path state :content)]
|
||||
(-> state
|
||||
(assoc-in [:workspace-local :edit-path id :old-content] content)
|
||||
(st/set-content (-> content
|
||||
@@ -353,10 +352,10 @@
|
||||
(rx/of (changes/save-path-content {:preserve-move-to true})))))
|
||||
|
||||
(defn create-node-at-position
|
||||
[event]
|
||||
[params]
|
||||
(ptk/reify ::create-node-at-position
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [id (st/get-path-id state)]
|
||||
(rx/of (dwsh/update-shapes [id] path/convert-to-path)
|
||||
(split-segments event))))))
|
||||
(split-segments id params))))))
|
||||
|
||||
@@ -193,16 +193,6 @@
|
||||
(when (:fill attributes) (update-fill value shape-ids attributes page-id))
|
||||
(when (:stroke-color attributes) (update-stroke-color value shape-ids attributes page-id)))))))
|
||||
|
||||
(defn update-shape-dimensions
|
||||
([value shape-ids attributes] (update-shape-dimensions value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(ptk/reify ::update-shape-dimensions
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (number? value)
|
||||
(rx/of
|
||||
(when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id}))
|
||||
(when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id}))))))))
|
||||
|
||||
(defn- attributes->layout-gap [attributes value]
|
||||
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
|
||||
@@ -250,21 +240,6 @@
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))))))))
|
||||
|
||||
(defn update-layout-spacing
|
||||
([value shape-ids attributes] (update-layout-spacing value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(ptk/reify ::update-layout-spacing
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (number? value)
|
||||
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)
|
||||
layout-attributes (attributes->layout-gap attributes value)]
|
||||
(rx/of
|
||||
(dwsl/update-layout ids-with-layout
|
||||
layout-attributes
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))))))))
|
||||
|
||||
(defn update-shape-position
|
||||
([value shape-ids attributes] (update-shape-position value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
@@ -278,6 +253,20 @@
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))))))))
|
||||
|
||||
(defn update-layout-gap
|
||||
[value shape-ids attributes page-id]
|
||||
(ptk/reify ::update-layout-gap
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (number? value)
|
||||
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)
|
||||
layout-attributes (attributes->layout-gap attributes value)]
|
||||
(rx/of
|
||||
(dwsl/update-layout ids-with-layout
|
||||
layout-attributes
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))))))
|
||||
|
||||
(defn update-layout-sizing-limits
|
||||
([value shape-ids attributes] (update-layout-sizing-limits value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
@@ -493,20 +482,127 @@
|
||||
value
|
||||
[shape-ids attributes page-id])))))
|
||||
|
||||
(defn update-typography-interactive
|
||||
([value shape-ids attributes] (update-typography value shape-ids attributes nil))
|
||||
(defn update-shape-dimensions
|
||||
([value shape-ids attributes] (update-shape-dimensions value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(when (map? value)
|
||||
(rx/merge
|
||||
(apply-functions-map
|
||||
{:font-size update-font-size
|
||||
:font-family update-font-family-interactive
|
||||
:font-weight update-font-weight-interactive
|
||||
:letter-spacing update-letter-spacing
|
||||
:text-case update-text-case
|
||||
:text-decoration update-text-decoration-interactive}
|
||||
value
|
||||
[shape-ids attributes page-id])))))
|
||||
(ptk/reify ::update-shape-dimensions
|
||||
ptk/WatchEvent
|
||||
(watch [_ _ _]
|
||||
(when (number? value)
|
||||
(rx/of
|
||||
(when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id}))
|
||||
(when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id}))))))))
|
||||
|
||||
(defn- attributes->actions
|
||||
[{:keys [value shape-ids attributes page-id]}]
|
||||
(cond-> []
|
||||
(some attributes #{:width :height})
|
||||
(conj #(update-shape-dimensions
|
||||
value shape-ids
|
||||
(set (filter attributes #{:width :height}))
|
||||
page-id))
|
||||
|
||||
(some attributes #{:x :y})
|
||||
(conj #(update-shape-position
|
||||
value shape-ids
|
||||
(set (filter attributes #{:x :y}))
|
||||
page-id))
|
||||
|
||||
(some attributes #{:p1 :p2 :p3 :p4})
|
||||
(conj #(update-layout-padding
|
||||
value shape-ids
|
||||
(set (filter attributes #{:p1 :p2 :p3 :p4}))
|
||||
page-id))
|
||||
|
||||
(some attributes #{:m1 :m2 :m3 :m4})
|
||||
(conj #(update-layout-item-margin
|
||||
value shape-ids
|
||||
(set (filter attributes #{:m1 :m2 :m3 :m4}))
|
||||
page-id))
|
||||
|
||||
(some attributes #{:row-gap :column-gap})
|
||||
(conj #(update-layout-gap
|
||||
value shape-ids
|
||||
(set (filter attributes #{:row-gap :column-gap}))
|
||||
page-id))
|
||||
|
||||
(some attributes #{:r1 :r2 :r3 :r4})
|
||||
(conj #(if (= attributes #{:r1 :r2 :r3 :r4})
|
||||
(update-shape-radius-all value shape-ids attributes page-id)
|
||||
(update-shape-radius-for-corners
|
||||
value shape-ids
|
||||
(set (filter attributes #{:r1 :r2 :r3 :r4}))
|
||||
page-id)))
|
||||
|
||||
(some attributes #{:stroke-width})
|
||||
(conj #(update-stroke-width
|
||||
value shape-ids
|
||||
#{:stroke-width}
|
||||
page-id))
|
||||
|
||||
(some attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w})
|
||||
(conj #(update-layout-sizing-limits
|
||||
value shape-ids
|
||||
(set (filter attributes #{:max-width :max-height :layout-item-max-h :layout-item-max-w :layout-item-min-h :layout-item-min-w}))
|
||||
page-id))))
|
||||
|
||||
(defn apply-dimensions-token
|
||||
([value shape-ids attributes] (apply-dimensions-token value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(ptk/reify ::apply-dimensions-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(when (number? value)
|
||||
(let [actions (attributes->actions
|
||||
{:value value
|
||||
:shape-ids shape-ids
|
||||
:attributes attributes
|
||||
:page-id page-id
|
||||
:state state})]
|
||||
(apply rx/of (map #(%) actions))))))))
|
||||
|
||||
(defn apply-spacing-token
|
||||
([value shape-ids attributes] (apply-spacing-token value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(ptk/reify ::apply-spacing-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [spacing-attrs
|
||||
#{:row-gap :column-gap
|
||||
:m1 :m2 :m3 :m4
|
||||
:p1 :p2 :p3 :p4}]
|
||||
(when (and (number? value)
|
||||
(set? attributes)
|
||||
(set/subset? attributes spacing-attrs))
|
||||
|
||||
(let [actions (attributes->actions
|
||||
{:value value
|
||||
:shape-ids shape-ids
|
||||
:attributes attributes
|
||||
:page-id page-id
|
||||
:state state})]
|
||||
(apply rx/of (map #(%) actions)))))))))
|
||||
|
||||
(defn apply-sizing-token
|
||||
([value shape-ids attributes] (apply-sizing-token value shape-ids attributes nil))
|
||||
([value shape-ids attributes page-id]
|
||||
(ptk/reify ::apply-sizing-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [sizing-attrs
|
||||
#{:width :height
|
||||
:max-width :max-height}]
|
||||
(when (and (number? value)
|
||||
(set? attributes)
|
||||
(set/subset? attributes sizing-attrs))
|
||||
|
||||
(let [actions (attributes->actions
|
||||
{:value value
|
||||
:shape-ids shape-ids
|
||||
:attributes attributes
|
||||
:page-id page-id
|
||||
:state state})]
|
||||
(apply rx/of (map #(%) actions)))))))))
|
||||
|
||||
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
|
||||
|
||||
@@ -572,13 +668,13 @@
|
||||
(rx/of res))))
|
||||
(rx/of (dwu/commit-undo-transaction undo-id)))))))))))))
|
||||
|
||||
(defn apply-spacing-token
|
||||
(defn apply-spacing-token-separated
|
||||
"Handles edge-case for spacing token when applying token via toggle button.
|
||||
Splits out `shape-ids` into seperate default actions:
|
||||
- Layouts take the `default` update function
|
||||
- Shapes inside layout will only take margin"
|
||||
[{:keys [token shapes attr]}]
|
||||
(ptk/reify ::apply-spacing-token
|
||||
(ptk/reify ::apply-spacing-token-separated
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
@@ -646,54 +742,19 @@
|
||||
:token token
|
||||
:shape-ids shape-ids}))
|
||||
(rx/of
|
||||
(case (:type token)
|
||||
:spacing
|
||||
(apply-spacing-token {:token token
|
||||
:attr attrs
|
||||
:shapes shapes})
|
||||
(cond
|
||||
(and (= (:type token) :spacing)
|
||||
(nil? attrs))
|
||||
(apply-spacing-token-separated {:token token
|
||||
:attr attrs
|
||||
:shapes shapes})
|
||||
|
||||
:else
|
||||
(apply-token {:attributes (if (empty? attrs) attributes attrs)
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape on-update-shape}))))))))
|
||||
|
||||
(defn toggle-border-radius-token
|
||||
[{:keys [token attrs shape-ids expand-with-children]}]
|
||||
(ptk/reify ::on-toggle-border-radius-token
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(let [objects (dsh/lookup-page-objects state)
|
||||
shapes (into [] (keep (d/getf objects)) shape-ids)
|
||||
|
||||
shapes
|
||||
(if expand-with-children
|
||||
(into []
|
||||
(mapcat (fn [shape]
|
||||
(if (= (:type shape) :group)
|
||||
(keep objects (:shapes shape))
|
||||
[shape])))
|
||||
shapes)
|
||||
shapes)
|
||||
|
||||
{:keys [attributes all-attributes]}
|
||||
(get token-properties (:type token))
|
||||
|
||||
unapply-tokens?
|
||||
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
|
||||
|
||||
shape-ids (map :id shapes)]
|
||||
|
||||
(if unapply-tokens?
|
||||
(rx/of
|
||||
(unapply-token {:attributes (or attrs all-attributes attributes)
|
||||
:token token
|
||||
:shape-ids shape-ids}))
|
||||
(rx/of
|
||||
(apply-token {:attributes attrs
|
||||
:token token
|
||||
:shape-ids shape-ids
|
||||
:on-update-shape update-shape-radius-for-corners})))))))
|
||||
|
||||
|
||||
(defn apply-token-on-selected
|
||||
[color-operations token]
|
||||
(ptk/reify ::apply-token-on-selected
|
||||
@@ -823,7 +884,7 @@
|
||||
{:title "Sizing"
|
||||
:attributes #{:width :height}
|
||||
:all-attributes ctt/sizing-keys
|
||||
:on-update-shape update-shape-dimensions
|
||||
:on-update-shape apply-sizing-token
|
||||
:modal {:key :tokens/sizing
|
||||
:fields [{:label "Sizing"
|
||||
:key :sizing}]}}
|
||||
@@ -836,7 +897,7 @@
|
||||
ctt/border-radius-keys
|
||||
ctt/axis-keys
|
||||
ctt/stroke-width-keys)
|
||||
:on-update-shape update-shape-dimensions
|
||||
:on-update-shape apply-dimensions-token
|
||||
:modal {:key :tokens/dimensions
|
||||
:fields [{:label "Dimensions"
|
||||
:key :dimensions}]}}
|
||||
@@ -869,7 +930,7 @@
|
||||
{:title "Spacing"
|
||||
:attributes #{:column-gap :row-gap}
|
||||
:all-attributes ctt/spacing-keys
|
||||
:on-update-shape update-layout-spacing
|
||||
:on-update-shape apply-spacing-token
|
||||
:modal {:key :tokens/spacing
|
||||
:fields [{:label "Spacing"
|
||||
:key :spacing}]}}))
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
{ctt/border-radius-keys dwta/update-shape-radius-for-corners
|
||||
ctt/color-keys dwta/update-fill-stroke
|
||||
ctt/stroke-width-keys dwta/update-stroke-width
|
||||
ctt/sizing-keys dwta/update-shape-dimensions
|
||||
ctt/sizing-keys dwta/apply-dimensions-token
|
||||
ctt/opacity-keys dwta/update-opacity
|
||||
ctt/rotation-keys dwta/update-rotation
|
||||
|
||||
@@ -73,8 +73,8 @@
|
||||
#{:x :y} dwta/update-shape-position
|
||||
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding
|
||||
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
|
||||
#{:column-gap :row-gap} dwta/update-layout-spacing
|
||||
#{:width :height} dwta/update-shape-dimensions
|
||||
#{:column-gap :row-gap} dwta/update-layout-gap
|
||||
#{:width :height} dwta/apply-dimensions-token
|
||||
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits})
|
||||
|
||||
(def ^:private attribute-actions-map
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
@@ -19,7 +20,6 @@
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[okulary.core :as l]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -138,13 +138,13 @@
|
||||
"&display=block")]
|
||||
(dm/str
|
||||
(-> cf/public-uri
|
||||
(assoc :path "/internal/gfonts/css")
|
||||
(u/join "internal/gfonts/css")
|
||||
(assoc :query query)))))
|
||||
|
||||
(defn- process-gfont-css
|
||||
[css]
|
||||
(let [base (dm/str (assoc cf/public-uri :path "/internal/gfonts/font"))]
|
||||
(str/replace css "https://fonts.gstatic.com/s" base)))
|
||||
(let [base (u/join cf/public-uri "internal/gfonts/font")]
|
||||
(str/replace css "https://fonts.gstatic.com/s" (dm/str base))))
|
||||
|
||||
(defn- fetch-gfont-css
|
||||
[url]
|
||||
@@ -178,7 +178,9 @@
|
||||
|
||||
(defn- asset-id->uri
|
||||
[asset-id]
|
||||
(str (u/join cf/public-uri "assets/by-id/" asset-id)))
|
||||
(-> cf/public-uri
|
||||
(u/join "assets/by-id/" asset-id)
|
||||
(str)))
|
||||
|
||||
(defn generate-custom-font-variant-css
|
||||
[family variant]
|
||||
@@ -370,7 +372,7 @@
|
||||
:else
|
||||
(let [{:keys [weight style suffix]} (get-variant font font-variant-id)
|
||||
suffix (or suffix font-variant-id)
|
||||
params {:uri (dm/str cf/public-uri "fonts/" family "-" suffix ".woff")
|
||||
params {:uri (str (u/join cf/public-uri (str "fonts/" family "-" suffix ".woff")))
|
||||
:family family
|
||||
:style style
|
||||
:weight weight}]
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
@@ -26,7 +27,9 @@
|
||||
(defonce instance nil)
|
||||
(defonce msgbus (rx/subject))
|
||||
(defonce origin
|
||||
(dm/str (assoc cf/rasterizer-uri :path "/rasterizer.html")))
|
||||
(-> cf/rasterizer-uri
|
||||
(u/join "rasterizer.html")
|
||||
(dm/str)))
|
||||
|
||||
(declare send-message!)
|
||||
|
||||
@@ -129,7 +132,9 @@
|
||||
(dom/append-child! js/document.body iframe)
|
||||
(set! instance iframe))
|
||||
|
||||
(let [new-origin (dm/str (assoc cf/public-uri :path "/rasterizer.html"))]
|
||||
(let [new-origin (-> cf/public-uri
|
||||
(u/join "rasterizer.html")
|
||||
(dm/str))]
|
||||
(log/warn :hint "fallback to main domain" :origin new-origin)
|
||||
|
||||
(dom/set-attribute! iframe "src" new-origin)
|
||||
|
||||
@@ -628,6 +628,7 @@
|
||||
width: $sz-400;
|
||||
padding: var(--sp-xxxl);
|
||||
background-color: var(--color-background-primary);
|
||||
z-index: var(--z-index-set);
|
||||
|
||||
&.hero {
|
||||
top: px2rem(216);
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
:float
|
||||
:string
|
||||
[:= :multiple]]]]
|
||||
[:text-icon {:optional true} :string]
|
||||
[:default {:optional true} [:maybe :string]]
|
||||
[:placeholder {:optional true} :string]
|
||||
[:icon {:optional true} [:maybe schema:icon]]
|
||||
@@ -216,7 +217,8 @@
|
||||
is-selected-on-focus nillable
|
||||
tokens applied-token empty-to-end
|
||||
on-change on-blur on-focus on-detach
|
||||
property align ref name]
|
||||
property align ref name
|
||||
text-icon]
|
||||
:rest props}]
|
||||
|
||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||
@@ -637,14 +639,23 @@
|
||||
:on-change store-raw-value
|
||||
:variant "comfortable"
|
||||
:disabled disabled
|
||||
:slot-start (when icon
|
||||
(mf/html [:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]]))
|
||||
:slot-start (when (or icon text-icon)
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]
|
||||
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)
|
||||
:aria-labelledby property}
|
||||
text-icon])]))
|
||||
:slot-end (when-not disabled
|
||||
(when (some? tokens)
|
||||
(mf/html [:> icon-button* {:variant "ghost"
|
||||
@@ -676,14 +687,23 @@
|
||||
:disabled disabled
|
||||
:on-blur on-blur
|
||||
:class inner-class
|
||||
:slot-start (when icon
|
||||
(mf/html [:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
[:> icon* {:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]]))
|
||||
:slot-start (when (or icon text-icon)
|
||||
(mf/html
|
||||
[:> tooltip*
|
||||
{:content property
|
||||
:id property}
|
||||
(cond
|
||||
icon
|
||||
[:> icon*
|
||||
{:icon-id icon
|
||||
:size "s"
|
||||
:aria-labelledby property
|
||||
:class (stl/css :icon)}]
|
||||
|
||||
text-icon
|
||||
[:div {:class (stl/css :text-icon)
|
||||
:aria-labelledby property}
|
||||
text-icon])]))
|
||||
:token-wrapper-ref token-wrapper-ref
|
||||
:token-detach-btn-ref token-detach-btn-ref
|
||||
:detach-token detach-token})))]
|
||||
@@ -718,21 +738,41 @@
|
||||
(mf/with-effect [dropdown-options]
|
||||
(mf/set-ref-val! options-ref dropdown-options))
|
||||
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:ref wrapper-ref}
|
||||
(if (some? icon)
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:ref wrapper-ref}
|
||||
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]))
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))]
|
||||
[:div {:class (dm/str class " " (stl/css :input-wrapper))
|
||||
:aria-labelledby property
|
||||
:ref wrapper-ref}
|
||||
|
||||
(if (and (some? token-applied)
|
||||
(not= :multiple token-applied))
|
||||
[:> token-field* token-props]
|
||||
[:> input-field* input-props])
|
||||
|
||||
(when ^boolean is-open
|
||||
(let [options (if (delay? dropdown-options) @dropdown-options dropdown-options)]
|
||||
[:> options-dropdown* {:on-click on-option-click
|
||||
:id listbox-id
|
||||
:options options
|
||||
:selected selected-id
|
||||
:focused focused-id
|
||||
:align align
|
||||
:empty-to-end empty-to-end
|
||||
:ref set-option-ref}]))])))
|
||||
|
||||
@@ -29,7 +29,14 @@
|
||||
|
||||
.icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
min-width: var(--sp-l);
|
||||
min-inline-size: var(--sp-l);
|
||||
}
|
||||
|
||||
.text-icon {
|
||||
color: var(--color-foreground-secondary);
|
||||
@include t.use-typography("code-font");
|
||||
inline-size: fit-content;
|
||||
min-inline-size: px2rem(40);
|
||||
}
|
||||
|
||||
.invisible-button {
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
--token-field-outline-color: none;
|
||||
--token-field-height: var(--sp-xxxl);
|
||||
--token-field-margin: unset;
|
||||
display: grid;
|
||||
display: inline-flex;
|
||||
column-gap: var(--sp-xs);
|
||||
align-items: center;
|
||||
position: relative;
|
||||
|
||||
@@ -151,7 +151,6 @@
|
||||
(st/emit! (dw/set-clipboard-style style))))]
|
||||
|
||||
(.addEventListener ^js global/document "keyup" on-key-up)
|
||||
(.addEventListener ^js instance "blur" on-blur)
|
||||
(.addEventListener ^js instance "focus" on-focus)
|
||||
(.addEventListener ^js instance "needslayout" on-needs-layout)
|
||||
(.addEventListener ^js instance "stylechange" on-style-change)
|
||||
@@ -166,8 +165,12 @@
|
||||
|
||||
;; This function is called when the component is unmounted
|
||||
(fn []
|
||||
;; Explicitly call on-blur here instead of relying on browser blur events,
|
||||
;; because in Firefox blur is not reliably fired when leaving the text editor
|
||||
;; by clicking elsewhere. The component does unmount when the shape is
|
||||
;; deselected, so we can safely call the blur handler here to finalize the editor.
|
||||
(on-blur)
|
||||
(.removeEventListener ^js global/document "keyup" on-key-up)
|
||||
(.removeEventListener ^js instance "blur" on-blur)
|
||||
(.removeEventListener ^js instance "focus" on-focus)
|
||||
(.removeEventListener ^js instance "needslayout" on-needs-layout)
|
||||
(.removeEventListener ^js instance "stylechange" on-style-change)
|
||||
|
||||
@@ -119,6 +119,7 @@
|
||||
[:button {:class (stl/css-case
|
||||
:toggle-content true
|
||||
:inverse expanded?)
|
||||
:aria-label "Toggle layer"
|
||||
:on-click on-toggle-collapse}
|
||||
deprecated-icon/arrow])
|
||||
|
||||
@@ -422,7 +423,8 @@
|
||||
(reset! observer-var nil))))
|
||||
|
||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
||||
(mf/with-effect [children-count expanded?]
|
||||
;; and (shapes item) to reconnect observer after shape changes
|
||||
(mf/with-effect [children-count expanded? (:shapes item)]
|
||||
(let [total (count (:shapes item))
|
||||
node (mf/ref-val ref)
|
||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||
|
||||
@@ -3,17 +3,15 @@
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.shape.radius :as ctsr]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[beicon.v2.core :as rx]
|
||||
[potok.v2.core :as ptk]
|
||||
@@ -46,63 +44,6 @@
|
||||
(identical? (get old-values :r4)
|
||||
(get new-values :r4)))))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach radius] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
r1-value (get applied-tokens :r1)
|
||||
all-token-equal? (and (seq applied-tokens) (all-equal? applied-tokens))
|
||||
all-values-equal? (all-equal? values)
|
||||
|
||||
applied-token (cond
|
||||
(not (seq applied-tokens))
|
||||
nil
|
||||
|
||||
(and (= radius :all) (or (not all-values-equal?) (not all-token-equal?)))
|
||||
:multiple
|
||||
|
||||
(and all-token-equal? all-values-equal? (= radius :all))
|
||||
r1-value
|
||||
|
||||
:else
|
||||
(get applied-tokens radius))
|
||||
|
||||
|
||||
placeholder (if (= radius :all)
|
||||
(cond
|
||||
(or (not all-values-equal?)
|
||||
(not all-token-equal?))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
|
||||
(cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--"))
|
||||
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder placeholder
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value values})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc border-radius-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-border-radius-menu-props)]}
|
||||
[{:keys [class ids values applied-tokens]}]
|
||||
@@ -110,6 +51,7 @@
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
all-values-equal? (all-equal? values)
|
||||
all-token-equal? (and (seq applied-tokens) (all-equal? applied-tokens))
|
||||
|
||||
radius-expanded* (mf/use-state false)
|
||||
radius-expanded (deref radius-expanded*)
|
||||
@@ -192,11 +134,10 @@
|
||||
(st/emit!
|
||||
(change-radius (fn [shape]
|
||||
(ctsr/set-radius-to-all-corners shape value))))
|
||||
(doseq [attr [:r1 :r2 :r3 :r4]]
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{:r1 :r2 :r3 :r4}
|
||||
:shape-ids ids})))))
|
||||
|
||||
|
||||
on-single-radius-change
|
||||
@@ -205,9 +146,10 @@
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (number? value))
|
||||
(st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr))
|
||||
(st/emit! (dwta/toggle-border-radius-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids})))))
|
||||
(st/emit! (st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-radius-r1-change #(on-single-radius-change % :r1)
|
||||
on-radius-r2-change #(on-single-radius-change % :r2)
|
||||
@@ -235,18 +177,31 @@
|
||||
:on-detach on-detach-all
|
||||
:icon i/corner-radius
|
||||
:min 0
|
||||
:name :border-radius
|
||||
:attr :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius")
|
||||
:class (stl/css :radius-wrapper)
|
||||
:applied-tokens applied-tokens
|
||||
:radius :all
|
||||
:applied-token (cond
|
||||
(not (seq applied-tokens))
|
||||
nil
|
||||
|
||||
(or (not all-values-equal?) (not all-token-equal?))
|
||||
:multiple
|
||||
|
||||
:else
|
||||
(get applied-tokens :r1))
|
||||
:align :right
|
||||
:values (if all-values-equal?
|
||||
(if (nil? (:r1 values))
|
||||
0
|
||||
(:r1 values))
|
||||
nil)}]
|
||||
:placeholder (cond
|
||||
(or (not all-values-equal?)
|
||||
(not all-token-equal?))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:value (if all-values-equal?
|
||||
(if (nil? (:r1 values))
|
||||
0
|
||||
(:r1 values))
|
||||
nil)}]
|
||||
|
||||
[:div {:class (stl/css :radius-1)
|
||||
:title (tr "workspace.options.radius")}
|
||||
@@ -276,56 +231,76 @@
|
||||
{:on-change on-radius-r1-change
|
||||
:on-detach on-detach-r1
|
||||
:min 0
|
||||
:name :border-radius
|
||||
:attr :border-radius
|
||||
:property (tr "workspace.options.radius-top-left")
|
||||
:applied-tokens applied-tokens
|
||||
:radius :r1
|
||||
:applied-token (get applied-tokens :r1)
|
||||
:align :right
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r1)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:class (stl/css :radius-wrapper :dropdown-offset)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:values (:r1 values)}]
|
||||
:value (:r1 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r2-change
|
||||
:on-detach on-detach-r2
|
||||
:min 0
|
||||
:name :border-radius
|
||||
:attr :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-top-right")
|
||||
:applied-tokens applied-tokens
|
||||
:applied-token (get applied-tokens :r2)
|
||||
:align :right
|
||||
:class (stl/css :radius-wrapper)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:radius :r2
|
||||
:values (:r2 values)}]
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r2)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:value (:r2 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r4-change
|
||||
:on-detach on-detach-r4
|
||||
:min 0
|
||||
:name :border-radius
|
||||
:attr :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-bottom-left")
|
||||
:applied-tokens applied-tokens
|
||||
:applied-token (get applied-tokens :r4)
|
||||
:class (stl/css :radius-wrapper :dropdown-offset)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:radius :r4
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r4)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:align :right
|
||||
:values (:r4 values)}]
|
||||
:value (:r4 values)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-radius-r3-change
|
||||
:on-detach on-detach-r3
|
||||
:min 0
|
||||
:name :border-radius
|
||||
:attr :border-radius
|
||||
:nillable true
|
||||
:property (tr "workspace.options.radius-bottom-right")
|
||||
:applied-tokens applied-tokens
|
||||
:radius :r3
|
||||
:applied-token (get applied-tokens :r3)
|
||||
:placeholder (cond
|
||||
(or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :r3)))
|
||||
(tr "settings.multiple")
|
||||
:else
|
||||
"--")
|
||||
:align :right
|
||||
:class (stl/css :radius-wrapper)
|
||||
:inner-class (stl/css :no-icon-input)
|
||||
:values (:r3 values)}]]
|
||||
:value (:r3 values)}]]
|
||||
|
||||
[:div {:class (stl/css :radius-4)}
|
||||
[:div {:class (stl/css :small-input)}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
(ns app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens
|
||||
(:require
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
[{:keys [value attr applied-token align on-detach placeholder input-type] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
|
||||
tokens (mf/with-memo [tokens input-type]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input (or input-type attr)))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach attr)
|
||||
#(on-detach % attr))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (or placeholder
|
||||
(if (= :multiple value)
|
||||
(tr "settings.multiple")
|
||||
"--"))
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name attr
|
||||
:value value})]
|
||||
[:> numeric-input* props]))
|
||||
@@ -9,7 +9,6 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
@@ -17,10 +16,9 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -62,36 +60,6 @@
|
||||
(identical? (get old-values :hidden)
|
||||
(get new-values :hidden)))))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr (mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
applied-token (get applied-tokens name)
|
||||
opacity-value (or (get values name) 1)
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple opacity-value))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name name
|
||||
:value (* 100 opacity-value)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc layer-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
||||
[{:keys [ids values applied-tokens]}]
|
||||
@@ -250,22 +218,24 @@
|
||||
:on-pointer-enter-option handle-blend-mode-enter
|
||||
:on-pointer-leave-option handle-blend-mode-leave}]]
|
||||
|
||||
|
||||
|
||||
(if token-numeric-inputs
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-opacity-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/percentage
|
||||
:min 0
|
||||
:max 100
|
||||
:name :opacity
|
||||
:attr :opacity
|
||||
:property (tr "workspace.options.opacity")
|
||||
:applied-tokens applied-tokens
|
||||
:applied-token (get applied-tokens :opacity)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (or (get values name) 1)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:align :right
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:values values}]
|
||||
:value (* 100
|
||||
(or (get values name) 1))}]
|
||||
|
||||
[:div {:class (stl/css :input)
|
||||
:title (tr "workspace.options.opacity")}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.math :as mth]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as tk]
|
||||
[app.config :as cf]
|
||||
[app.main.data.event :as-alias ev]
|
||||
[app.main.data.workspace :as udw]
|
||||
@@ -25,15 +24,14 @@
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.button :refer [button*]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.formats :as fmt]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
@@ -48,44 +46,6 @@
|
||||
:column i/column
|
||||
:column-reverse i/column-reverse))
|
||||
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
input-type (cond
|
||||
(some #{:p2 :p4} [name])
|
||||
:horizontal-padding
|
||||
|
||||
(some #{:p1 :p3} [name])
|
||||
:vertical-padding
|
||||
:else
|
||||
name)
|
||||
|
||||
tokens (mf/with-memo [tokens input-type]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input input-type))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name))
|
||||
(nil? (get values name)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:class (stl/css :numeric-input-layout)
|
||||
:applied-token (get applied-tokens name)
|
||||
:tokens tokens
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value (get values name)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
;; FLEX COMPONENTS
|
||||
|
||||
(def layout-container-flex-attrs
|
||||
@@ -366,15 +326,15 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr event]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :simple attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))
|
||||
updated-attr (if (= :p1 attr) #{:p1 :p3} #{:p2 :p4})]
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs updated-attr
|
||||
:shape-ids ids}))
|
||||
(on-change :simple attr resolved-value event))))))
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs (if (= :p1 attr)
|
||||
#{:p1 :p3}
|
||||
#{:p2 :p4})
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
@@ -415,11 +375,17 @@
|
||||
:on-focus on-focus-p1
|
||||
:icon i/padding-top-bottom
|
||||
:min 0
|
||||
:name :p1
|
||||
:attr :p1
|
||||
:input-type :vertical-padding
|
||||
:property (tr "workspace.layout-grid.editor.padding.vertical")
|
||||
:nillable true
|
||||
:applied-tokens {:p1 applied-to-p1}
|
||||
:values {:p1 p1}}]
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p1))
|
||||
(= :multiple p1)
|
||||
(nil? p1))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-to-p1
|
||||
:value p1}]
|
||||
|
||||
[:div {:class (stl/css :padding-simple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.vertical")}
|
||||
@@ -444,12 +410,18 @@
|
||||
:on-focus on-focus-p2
|
||||
:icon i/padding-left-right
|
||||
:min 0
|
||||
:name :p2
|
||||
:attr :p2
|
||||
:input-type :horizontal-padding
|
||||
:align :right
|
||||
:property (tr "workspace.layout-grid.editor.padding.horizontal")
|
||||
:nillable true
|
||||
:applied-tokens {:p2 applied-to-p2}
|
||||
:values {:p2 p2}}]
|
||||
:applied-token applied-to-p2
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p2))
|
||||
(= :multiple p2)
|
||||
(nil? p2))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:value p2}]
|
||||
|
||||
[:div {:class (stl/css :padding-simple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.horizontal")}
|
||||
@@ -480,14 +452,12 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr event]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change :multiple attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))
|
||||
(on-change :multiple attr resolved-value event))))))
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
@@ -537,10 +507,15 @@
|
||||
:on-focus on-focus-p1
|
||||
:icon i/padding-top
|
||||
:min 0
|
||||
:name :p1
|
||||
:attr :p1
|
||||
:input-type :vertical-padding
|
||||
:property (tr "workspace.layout-grid.editor.padding.top")
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p1))
|
||||
(= :multiple p1))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p1
|
||||
:value p1}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.top")}
|
||||
@@ -565,11 +540,16 @@
|
||||
:on-focus on-focus-p2
|
||||
:icon i/padding-right
|
||||
:min 0
|
||||
:name :p2
|
||||
:attr :p2
|
||||
:input-type :horizontal-padding
|
||||
:align :right
|
||||
:property (tr "workspace.layout-grid.editor.padding.right")
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p2))
|
||||
(= :multiple p2))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p2
|
||||
:value p2}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.right")}
|
||||
@@ -594,10 +574,15 @@
|
||||
:on-focus on-focus-p3
|
||||
:icon i/padding-bottom
|
||||
:min 0
|
||||
:name :p3
|
||||
:attr :p3
|
||||
:input-type :vertical-padding
|
||||
:property (tr "workspace.layout-grid.editor.padding.bottom")
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p3))
|
||||
(= :multiple p3))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p3
|
||||
:value p3}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.bottom")}
|
||||
@@ -623,10 +608,15 @@
|
||||
:icon i/padding-left
|
||||
:min 0
|
||||
:align :right
|
||||
:name :p4
|
||||
:attr :p4
|
||||
:input-type :horizontal-padding
|
||||
:property (tr "workspace.layout-grid.editor.padding.left")
|
||||
:applied-tokens applied-tokens
|
||||
:values value}]
|
||||
:placeholder (if (or (= :multiple (:applied-tokens p3))
|
||||
(= :multiple p3))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token p3
|
||||
:value p3}]
|
||||
|
||||
[:div {:class (stl/css :padding-multiple)
|
||||
:title (tr "workspace.layout-grid.editor.padding.left")}
|
||||
@@ -713,14 +703,15 @@
|
||||
(mf/use-fn
|
||||
(mf/deps on-change wrap-type ids)
|
||||
(fn [value event attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(if (or (string? value) (number? value))
|
||||
(on-change (= "nowrap" wrap-type) attr value event)
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))
|
||||
(on-change (= "nowrap" wrap-type) attr resolved-value event))))))
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs (if (= "nowrap" wrap-type)
|
||||
#{:row-gap :colum-gap}
|
||||
#{attr})
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
@@ -758,11 +749,16 @@
|
||||
:icon i/gap-vertical
|
||||
:nillable true
|
||||
:min 0
|
||||
:name :row-gap
|
||||
:applied-tokens applied-tokens
|
||||
:attr :row-gap
|
||||
:property "Row gap"
|
||||
:values {:row-gap (:row-gap value)}
|
||||
:disabled row-gap-disabled?}]
|
||||
:disabled row-gap-disabled?
|
||||
:placeholder (if (or (= :multiple (:applied-tokens (:row-gap value)))
|
||||
(= :multiple (:row-gap value)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token (:row-gap applied-tokens)
|
||||
:value (:row-gap value)}]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
:row-gap true
|
||||
@@ -792,11 +788,15 @@
|
||||
:icon i/gap-horizontal
|
||||
:nillable true
|
||||
:min 0
|
||||
:name :column-gap
|
||||
:attr :column-gap
|
||||
:align :right
|
||||
:applied-tokens applied-tokens
|
||||
:property "Column gap"
|
||||
:values {:column-gap (:column-gap value)}
|
||||
:placeholder (if (or (= :multiple (:applied-tokens (:column-gap value)))
|
||||
(= :multiple (:column-gap value)))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token (:column-gap applied-tokens)
|
||||
:value (:column-gap value)
|
||||
:disabled col-gap-disabled?}]
|
||||
|
||||
[:div {:class (stl/css-case
|
||||
|
||||
@@ -11,6 +11,8 @@
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
@@ -19,6 +21,7 @@
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.layout-container :refer [get-layout-flex-icon]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -46,147 +49,345 @@
|
||||
(select-margins (= prop :m1) (= prop :m2) (= prop :m3) (= prop :m4)))
|
||||
|
||||
(mf/defc margin-simple*
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
[{:keys [value on-change on-blur applied-tokens ids]}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
m3 (:m3 value)
|
||||
m4 (:m4 value)
|
||||
|
||||
m1-placeholder (if (and (not= value :multiple) (not= m1 m3)) (tr "settings.multiple") "--")
|
||||
m2-placeholder (if (and (not= value :multiple) (not= m2 m4)) (tr "settings.multiple") "--")
|
||||
|
||||
m1 (when (and (not= value :multiple) (= m1 m3)) m1)
|
||||
m2 (when (and (not= value :multiple) (= m2 m4)) m2)
|
||||
|
||||
token-applied-m1 (:m1 applied-tokens)
|
||||
token-applied-m2 (:m2 applied-tokens)
|
||||
token-applied-m3 (:m3 applied-tokens)
|
||||
token-applied-m4 (:m4 applied-tokens)
|
||||
|
||||
token-applied-m1 (if (and (not= applied-tokens :multiple) (= token-applied-m1 token-applied-m3)) token-applied-m1
|
||||
:multiple)
|
||||
|
||||
token-applied-m2 (if (and (not= applied-tokens :multiple) (= token-applied-m2 token-applied-m4)) token-applied-m2
|
||||
:multiple)
|
||||
|
||||
m1-placeholder (if (and (not= value :multiple)
|
||||
(= m1 m3)
|
||||
(= token-applied-m1 token-applied-m3))
|
||||
"--"
|
||||
(tr "settings.multiple"))
|
||||
|
||||
m2-placeholder (if (and (not= value :multiple)
|
||||
(= m2 m4)
|
||||
(= token-applied-m2 token-applied-m4))
|
||||
"--"
|
||||
(tr "settings.multiple"))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(case attr
|
||||
:m1 (select-margins true false true false)
|
||||
:m2 (select-margins false true false true))
|
||||
(fn [attr event]
|
||||
(case attr
|
||||
:m1 (select-margins true false true false)
|
||||
:m2 (select-margins false true false true))
|
||||
(dom/select-target event)))
|
||||
|
||||
(dom/select-target event))))
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-detach-horizontal
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach-token)
|
||||
(fn [token]
|
||||
(prn "token" token)
|
||||
(run! #(on-detach-token token %) [:m2 :m4])))
|
||||
|
||||
on-detach-vertical
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach-token)
|
||||
(fn [token]
|
||||
(run! #(on-detach-token token %) [:m1 :m3])))
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(on-change :simple attr value))))]
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(on-change :simple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs (if (= :m1 attr)
|
||||
#{:m1 :m3}
|
||||
#{:m2 :m4})
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-focus-m1
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m2
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
|
||||
on-m1-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
|
||||
|
||||
on-m2-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))]
|
||||
|
||||
[:div {:class (stl/css :margin-simple)}
|
||||
[:div {:class (stl/css :vertical-margin)
|
||||
:title "Vertical margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m1-placeholder
|
||||
:data-name "m1"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]]
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m1-change
|
||||
:on-detach on-detach-vertical
|
||||
:class (stl/css :vertical-margin-wrapper)
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m1
|
||||
:placeholder m1-placeholder
|
||||
:icon i/margin-top-bottom
|
||||
:min 0
|
||||
:attr :m1
|
||||
:input-type :vertical-margin
|
||||
:property "Vertical margin "
|
||||
:nillable true
|
||||
:applied-token token-applied-m1
|
||||
:value m1}]
|
||||
|
||||
[:div {:class (stl/css :horizontal-margin)
|
||||
:title "Horizontal margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m2-placeholder
|
||||
:data-name "m2"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]]]))
|
||||
[:div {:class (stl/css :vertical-margin)
|
||||
:title "Vertical margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m1-placeholder
|
||||
:data-name "m1"
|
||||
:on-focus on-focus-m1
|
||||
:on-change on-m1-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m2-change
|
||||
:on-detach on-detach-horizontal
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m2
|
||||
:placeholder m2-placeholder
|
||||
:icon i/margin-left-right
|
||||
:class (stl/css :horizontal-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m2
|
||||
:align :right
|
||||
:input-type :horizontal-margin
|
||||
:property "Horizontal margin"
|
||||
:nillable true
|
||||
:applied-token token-applied-m2
|
||||
:value m2}]
|
||||
|
||||
[:div {:class (stl/css :horizontal-margin)
|
||||
:title "Horizontal margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder m2-placeholder
|
||||
:data-name "m2"
|
||||
:on-focus on-focus-m2
|
||||
:on-change on-m2-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]])]))
|
||||
|
||||
(mf/defc margin-multiple*
|
||||
[{:keys [value on-change on-blur]}]
|
||||
(let [m1 (:m1 value)
|
||||
[{:keys [value on-change on-blur applied-tokens ids]}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
m1 (:m1 value)
|
||||
m2 (:m2 value)
|
||||
m3 (:m3 value)
|
||||
m4 (:m4 value)
|
||||
|
||||
applied-token-to-m1 (:m1 applied-tokens)
|
||||
applied-token-to-m2 (:m2 applied-tokens)
|
||||
applied-token-to-m3 (:m3 applied-tokens)
|
||||
applied-token-to-m4 (:m4 applied-tokens)
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-focus
|
||||
(mf/use-fn
|
||||
(fn [event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(select-margin attr)
|
||||
(dom/select-target event))))
|
||||
(fn [attr event]
|
||||
(select-margin attr)
|
||||
(dom/select-target event)))
|
||||
|
||||
on-focus-m1
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m2
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
|
||||
on-focus-m3
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m1))
|
||||
|
||||
on-focus-m4
|
||||
(mf/use-fn (mf/deps on-focus) #(on-focus :m2))
|
||||
|
||||
on-change'
|
||||
(mf/use-fn
|
||||
(mf/deps on-change)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "name")
|
||||
(keyword))]
|
||||
(on-change :multiple attr value))))]
|
||||
(mf/deps on-change ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(on-change :multiple attr value)
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
|
||||
on-m1-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m1))
|
||||
|
||||
on-m2-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m2))
|
||||
|
||||
on-m3-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m3))
|
||||
|
||||
on-m4-change
|
||||
(mf/use-fn (mf/deps on-change') #(on-change' % :m4))]
|
||||
|
||||
[:div {:class (stl/css :margin-multiple)}
|
||||
[:div {:class (stl/css :top-margin)
|
||||
:title "Top margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m1"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]]
|
||||
[:div {:class (stl/css :right-margin)
|
||||
:title "Right margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m2"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]]
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m1-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m1
|
||||
:icon i/margin-top
|
||||
:class (stl/css :top-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m1
|
||||
:input-type :vertical-margin
|
||||
:property "Top margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m1
|
||||
:value m1}]
|
||||
|
||||
[:div {:class (stl/css :bottom-margin)
|
||||
:title "Bottom margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m3"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m3}]]
|
||||
[:div {:class (stl/css :top-margin)
|
||||
:title "Top margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-top]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m1"
|
||||
:on-focus on-focus-m1
|
||||
:on-change on-m1-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m1}]])
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m2-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m2
|
||||
:icon i/margin-right
|
||||
:class (stl/css :right-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m2
|
||||
:align :right
|
||||
:input-type :horizontal-margin
|
||||
:property "Right margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m2
|
||||
:value m2}]
|
||||
|
||||
[:div {:class (stl/css :left-margin)
|
||||
:title "Left margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m4"
|
||||
:on-focus on-focus
|
||||
:on-change on-change'
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m4}]]]))
|
||||
[:div {:class (stl/css :right-margin)
|
||||
:title "Right margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-right]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m2"
|
||||
:on-focus on-focus-m2
|
||||
:on-change on-m2-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m2}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m3-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m3
|
||||
:icon i/margin-bottom
|
||||
:class (stl/css :bottom-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m3
|
||||
:align :right
|
||||
:input-type :vertical-margin
|
||||
:property "Bottom margin"
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m3
|
||||
:value m3}]
|
||||
|
||||
[:div {:class (stl/css :bottom-margin)
|
||||
:title "Bottom margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-bottom]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m3"
|
||||
:on-focus on-focus-m3
|
||||
:on-change on-m3-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m3}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-m4-change
|
||||
:on-detach on-detach-token
|
||||
:on-blur on-blur
|
||||
:on-focus on-focus-m4
|
||||
:icon i/margin-left
|
||||
:class (stl/css :left-margin-wrapper)
|
||||
:min 0
|
||||
:attr :m4
|
||||
:property "Left margin"
|
||||
:input-type :horizontal-margin
|
||||
:nillable true
|
||||
:applied-token applied-token-to-m4
|
||||
:value m4}]
|
||||
|
||||
[:div {:class (stl/css :left-margin)
|
||||
:title "Left margin"}
|
||||
[:span {:class (stl/css :icon)}
|
||||
deprecated-icon/margin-left]
|
||||
[:> deprecated-input/numeric-input* {:class (stl/css :numeric-input)
|
||||
:placeholder "--"
|
||||
:data-name "m4"
|
||||
:on-focus on-focus-m4
|
||||
:on-change on-m4-change
|
||||
:on-blur on-blur
|
||||
:nillable true
|
||||
:value m4}]])]))
|
||||
|
||||
(mf/defc margin-section*
|
||||
{::mf/private true
|
||||
::mf/expect-props #{:value :type :on-type-change :on-change}}
|
||||
::mf/expect-props #{:value :type :on-type-change :on-change :applied-tokens :ids}}
|
||||
[{:keys [type on-type-change] :as props}]
|
||||
(let [type (d/nilv type :simple)
|
||||
on-blur (mf/use-fn #(select-margins false false false false))
|
||||
@@ -292,8 +493,191 @@
|
||||
:label "Align self end"
|
||||
:value "end"}]}])
|
||||
|
||||
(mf/defc layout-size-constraints*
|
||||
{::mf/private true
|
||||
::mf/expect-props #{:value :applied-tokens :ids :v-sizing}}
|
||||
[{:keys [values v-sizing ids applied-tokens] :as props}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
min-w (get values :layout-item-min-w)
|
||||
|
||||
max-w (get values :layout-item-max-w)
|
||||
|
||||
min-h (get values :layout-item-min-h)
|
||||
|
||||
max-h (get values :layout-item-max-h)
|
||||
|
||||
applied-token-to-min-w (get applied-tokens :layout-item-min-w)
|
||||
|
||||
applied-token-to-max-w (get applied-tokens :layout-item-max-w)
|
||||
|
||||
applied-token-to-min-h (get applied-tokens :layout-item-min-h)
|
||||
|
||||
applied-token-to-max-h (get applied-tokens :layout-item-max-h)
|
||||
|
||||
on-detach-token
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [token attr]
|
||||
(st/emit! (dwta/unapply-token {:token (first token)
|
||||
:attributes #{attr}
|
||||
:shape-ids ids}))))
|
||||
|
||||
on-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (int? value))
|
||||
(st/emit! (dwsl/update-layout-child ids {attr value}))
|
||||
(do
|
||||
(st/emit!
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
on-layout-item-min-w-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-w))
|
||||
|
||||
on-layout-item-max-w-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-w))
|
||||
|
||||
on-layout-item-min-h-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-min-h))
|
||||
|
||||
on-layout-item-max-h-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :layout-item-max-h))]
|
||||
|
||||
[:div {:class (stl/css :advanced-options)}
|
||||
(when (= (:layout-item-h-sizing values) :fill)
|
||||
[:div {:class (stl/css :horizontal-fill)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-min-w-change
|
||||
:on-detach on-detach-token
|
||||
:class (stl/css :min-w-wrapper)
|
||||
:min 0
|
||||
:attr :layout-item-min-w
|
||||
:property (tr "workspace.options.layout-item.layout-item-min-w")
|
||||
:text-icon "MIN W"
|
||||
:input-type :min-width
|
||||
:nillable true
|
||||
:applied-token applied-token-to-min-w
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:value min-w}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-min-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-w")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-min-w-change
|
||||
:value (get values :layout-item-min-w)
|
||||
:nillable true}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-max-w-change
|
||||
:on-detach on-detach-token
|
||||
:text-icon "MAX W"
|
||||
:class (stl/css :max-w-wrapper)
|
||||
:min 0
|
||||
:input-type :max-width
|
||||
:attr :layout-item-max-w
|
||||
:property (tr "workspace.options.layout-item.layout-item-max-w")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:applied-token applied-token-to-max-w
|
||||
:value max-w}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-w")}
|
||||
[:span {:class (stl/css :icon-text)} "MAX W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-max-w-change
|
||||
:value (get values :layout-item-max-w)
|
||||
:nillable true}]])])
|
||||
|
||||
(when (= v-sizing :fill)
|
||||
[:div {:class (stl/css :vertical-fill)}
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-min-h-change
|
||||
:on-detach on-detach-token
|
||||
:text-icon "MIN H"
|
||||
:input-type :max-height
|
||||
:class (stl/css :min-h-wrapper)
|
||||
:min 0
|
||||
:attr :layout-item-min-h
|
||||
:property (tr "workspace.options.layout-item.layout-item-min-h")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
|
||||
:applied-token applied-token-to-min-h
|
||||
:value min-h}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-min-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-h")}
|
||||
[:span {:class (stl/css :icon-text)} "MIN H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-min-h-change
|
||||
:value (get values :layout-item-min-h)
|
||||
:nillable true}]])
|
||||
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-layout-item-max-h-change
|
||||
:on-detach on-detach-token
|
||||
:class (stl/css :max-h-wrapper)
|
||||
:min 0
|
||||
:text-icon "MAX H"
|
||||
:input-type :max-height
|
||||
:attr :layout-item-max-h
|
||||
:property (tr "workspace.options.layout-item.layout-item-max-h")
|
||||
:nillable true
|
||||
:tooltip-class (stl/css :tooltip-wrapper)
|
||||
:applied-token applied-token-to-max-h
|
||||
:value max-h}]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MAX H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-layout-item-max-h-change
|
||||
:value (get values :layout-item-max-h)
|
||||
:nillable true}]])])]))
|
||||
|
||||
(mf/defc layout-item-menu
|
||||
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout?}
|
||||
{::mf/memo #{:ids :values :type :is-layout-child? :is-grid-parent :is-flex-parent? :is-grid-layout? :is-flex-layout? :applied-tokens}
|
||||
::mf/props :obj}
|
||||
[{:keys [ids values
|
||||
^boolean is-layout-child?
|
||||
@@ -301,7 +685,8 @@
|
||||
^boolean is-grid-parent?
|
||||
^boolean is-flex-parent?
|
||||
^boolean is-flex-layout?
|
||||
^boolean is-grid-layout?]}]
|
||||
^boolean is-grid-layout?
|
||||
applied-tokens]}]
|
||||
|
||||
(let [selection-parents* (mf/use-memo (mf/deps ids) #(refs/parents-by-ids ids))
|
||||
selection-parents (mf/deref selection-parents*)
|
||||
@@ -397,16 +782,7 @@
|
||||
(fn [value]
|
||||
(st/emit! (dwsl/update-layout-child ids {:layout-item-v-sizing (keyword value)}))))
|
||||
|
||||
;; Size and position
|
||||
on-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value event]
|
||||
(let [attr (-> (dom/get-current-target event)
|
||||
(dom/get-data "attr")
|
||||
(keyword))]
|
||||
(st/emit! (dwsl/update-layout-child ids {attr value})))))
|
||||
|
||||
;; Position
|
||||
on-change-position
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
@@ -423,7 +799,8 @@
|
||||
(fn [value]
|
||||
(st/emit! (dwsl/update-layout-child ids {:layout-item-z-index value}))))]
|
||||
|
||||
[:div {:class (stl/css :element-set)}
|
||||
[:section {:class (stl/css :element-set)
|
||||
:aria-label "layout item menu"}
|
||||
[:div {:class (stl/css :element-title)}
|
||||
[:> title-bar* {:collapsable has-content?
|
||||
:collapsed (not open?)
|
||||
@@ -483,74 +860,13 @@
|
||||
[:> margin-section* {:value (:layout-item-margin values)
|
||||
:type (:layout-item-margin-type values)
|
||||
:on-type-change on-margin-type-change
|
||||
:applied-tokens applied-tokens
|
||||
:ids ids
|
||||
:on-change on-margin-change}])
|
||||
|
||||
(when (or (= h-sizing :fill)
|
||||
(= v-sizing :fill))
|
||||
[:div {:class (stl/css :advanced-options)}
|
||||
(when (= (:layout-item-h-sizing values) :fill)
|
||||
[:div {:class (stl/css :horizontal-fill)}
|
||||
[:div {:class (stl/css :layout-item-min-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-w")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-min-w)
|
||||
:nillable true}]]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-w)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-w")}
|
||||
[:span {:class (stl/css :icon-text)} "MAX W"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-w"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-max-w)
|
||||
:nillable true}]]])
|
||||
|
||||
(when (= v-sizing :fill)
|
||||
[:div {:class (stl/css :vertical-fill)}
|
||||
[:div {:class (stl/css :layout-item-min-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-min-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MIN H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-min-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-min-h)
|
||||
:nillable true}]]
|
||||
|
||||
[:div {:class (stl/css :layout-item-max-h)
|
||||
:title (tr "workspace.options.layout-item.layout-item-max-h")}
|
||||
|
||||
[:span {:class (stl/css :icon-text)} "MAX H"]
|
||||
[:> deprecated-input/numeric-input*
|
||||
{:class (stl/css :numeric-input)
|
||||
:no-validate true
|
||||
:min 0
|
||||
:data-wrap true
|
||||
:placeholder "--"
|
||||
:data-attr "layout-item-max-h"
|
||||
:on-focus dom/select-target
|
||||
:on-change on-size-change
|
||||
:value (get values :layout-item-max-h)
|
||||
:nillable true}]]])])])]))
|
||||
[:> layout-size-constraints* {:ids ids
|
||||
:values values
|
||||
:applied-tokens applied-tokens
|
||||
:v-sizing v-sizing}])])]))
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.element-set {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.title-spacing-layout-element {
|
||||
margin: 0 0 deprecated.$s-4 0;
|
||||
margin: 0 0 var(--sp-xs) 0;
|
||||
}
|
||||
|
||||
.title-spacing-empty {
|
||||
padding-left: deprecated.$s-2;
|
||||
padding-inline-start: var(--sp-xxs);
|
||||
}
|
||||
|
||||
.flex-element-menu {
|
||||
@@ -35,8 +39,8 @@
|
||||
}
|
||||
|
||||
.z-index-wrapper {
|
||||
@include use-typography("body-small");
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
grid-column: 6 / span 3;
|
||||
}
|
||||
|
||||
@@ -55,7 +59,7 @@
|
||||
}
|
||||
|
||||
.position-options {
|
||||
width: 100%;
|
||||
inline-size: 100%;
|
||||
grid-column: 1 / span 5;
|
||||
}
|
||||
|
||||
@@ -75,7 +79,7 @@
|
||||
.vertical-margin,
|
||||
.horizontal-margin {
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
@include use-typography("body-small");
|
||||
}
|
||||
.vertical-margin {
|
||||
grid-column: 1;
|
||||
@@ -85,6 +89,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
.vertical-margin-wrapper {
|
||||
grid-column: 1;
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
}
|
||||
|
||||
.horizontal-margin-wrapper {
|
||||
grid-column: 2;
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
}
|
||||
|
||||
.margin-multiple {
|
||||
display: grid;
|
||||
grid-template-columns: subgrid;
|
||||
@@ -96,25 +110,33 @@
|
||||
.left-margin,
|
||||
.right-margin {
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
@include use-typography("body-small");
|
||||
}
|
||||
|
||||
.top-margin {
|
||||
.top-margin,
|
||||
.top-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
grid-column: 1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.bottom-margin {
|
||||
.bottom-margin,
|
||||
.bottom-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
grid-column: 2;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.left-margin {
|
||||
.left-margin,
|
||||
.left-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.right-margin {
|
||||
.right-margin,
|
||||
.right-margin-wrapper {
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
@@ -140,14 +162,18 @@
|
||||
.layout-item-max-w,
|
||||
.layout-item-max-h {
|
||||
@extend .input-element;
|
||||
@include deprecated.bodySmallTypography;
|
||||
@include use-typography("body-small");
|
||||
.icon-text {
|
||||
justify-content: flex-start;
|
||||
width: deprecated.$s-80;
|
||||
padding-top: deprecated.$s-2;
|
||||
inline-size: px2rem(80);
|
||||
padding-block-start: var(--sp-xxs);
|
||||
}
|
||||
}
|
||||
|
||||
.inputs-wrapper {
|
||||
grid-column: 1 / span 2;
|
||||
}
|
||||
|
||||
.tooltip-wrapper {
|
||||
inline-size: 100%;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
[app.common.geom.shapes :as gsh]
|
||||
[app.common.logic.shapes :as cls]
|
||||
[app.common.types.shape.layout :as ctl]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.constants :refer [size-presets]]
|
||||
[app.main.data.workspace :as udw]
|
||||
[app.main.data.workspace.interactions :as dwi]
|
||||
@@ -26,13 +25,12 @@
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.workspace.sidebar.options.menus.border-radius :refer [border-radius-menu*]]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[clojure.set :as set]
|
||||
@@ -91,32 +89,6 @@
|
||||
shape)]
|
||||
(select-keys shape measure-attrs)))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values name)))
|
||||
(tr "settings.multiple") "--")
|
||||
:class (stl/css :numeric-input-measures)
|
||||
:applied-token (get applied-tokens name)
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:value (get values name)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(def ^:private xf:map-type (map :type))
|
||||
(def ^:private xf:mapcat-type-to-options (mapcat type->options))
|
||||
|
||||
@@ -284,28 +256,17 @@
|
||||
(st/emit! (udw/change-orientation ids (keyword orientation)))))
|
||||
|
||||
;; SIZE AND PROPORTION LOCK
|
||||
do-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value attr]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(udw/update-dimensions ids attr value))))
|
||||
|
||||
on-size-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids shapes)
|
||||
(fn [value attr]
|
||||
(if (or (string? value) (number? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-size-change value attr) shapes))
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))
|
||||
(run! #(do-size-change resolved-value attr) shapes))))))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(udw/update-dimensions ids attr value))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids})))))
|
||||
|
||||
on-proportion-lock-change
|
||||
(mf/use-fn
|
||||
@@ -315,11 +276,6 @@
|
||||
(run! #(st/emit! (udw/set-shape-proportion-lock % new-lock)) ids))))
|
||||
|
||||
;; POSITION
|
||||
do-position-change
|
||||
(mf/use-fn
|
||||
(fn [shape' value attr]
|
||||
(st/emit! (udw/update-position (:id shape') {attr value}))))
|
||||
|
||||
on-position-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
@@ -327,21 +283,11 @@
|
||||
(if (or (string? value) (number? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-position-change %1 value attr) shapes))
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids}))
|
||||
(run! #(do-position-change %1 resolved-value attr) shapes))))))
|
||||
|
||||
;; ROTATION
|
||||
do-rotation-change
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
(fn [value]
|
||||
(st/emit! (udw/increase-rotation ids value))))
|
||||
(st/emit! (udw/update-position ids {attr value})))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{attr}
|
||||
:shape-ids ids})))))
|
||||
|
||||
on-rotation-change
|
||||
(mf/use-fn
|
||||
@@ -350,14 +296,11 @@
|
||||
(if (or (string? value) (number? value))
|
||||
(do
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids))
|
||||
(run! #(do-rotation-change value) shapes))
|
||||
(do
|
||||
(let [resolved-value (:resolved-value (first value))]
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{:rotation}
|
||||
:shape-ids ids}))
|
||||
(run! #(do-rotation-change resolved-value) shapes))))))
|
||||
(st/emit! (udw/increase-rotation ids value)))
|
||||
(st/emit! (udw/trigger-bounding-box-cloaking ids)
|
||||
(dwta/toggle-token {:token (first value)
|
||||
:attrs #{:rotation}
|
||||
:shape-ids ids})))))
|
||||
|
||||
on-width-change
|
||||
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :width))
|
||||
@@ -410,7 +353,8 @@
|
||||
(fn []
|
||||
(st/emit! (dwt/selected-fit-content))))]
|
||||
|
||||
[:div {:class (stl/css :element-set)}
|
||||
[:section {:class (stl/css :element-set)
|
||||
:aria-label "shape-measures-section"}
|
||||
(when (and (options :presets)
|
||||
(or (nil? all-types) (= (count all-types) 1)))
|
||||
[:div {:class (stl/css :presets)}
|
||||
@@ -472,10 +416,13 @@
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-w
|
||||
:min 0.01
|
||||
:name :width
|
||||
:attr :width
|
||||
:property (tr "workspace.options.width")
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
:applied-token (get applied-tokens :width)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :width)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :width)}]
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:disabled disabled-height-sizing?
|
||||
@@ -483,11 +430,11 @@
|
||||
:on-detach on-detach-token
|
||||
:min 0.01
|
||||
:icon i/character-h
|
||||
:name :height
|
||||
:attr :height
|
||||
:align :right
|
||||
:property (tr "workspace.options.height")
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]]
|
||||
:applied-token (get applied-tokens :height)
|
||||
:value (get values :height)}]]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css-case :width true
|
||||
@@ -531,20 +478,26 @@
|
||||
:on-change on-pos-x-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-x
|
||||
:name :x
|
||||
:attr :x
|
||||
:property (tr "workspace.options.x")
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
:applied-token (get applied-tokens :x)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :x)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :x)}]
|
||||
[:> numeric-input-wrapper*
|
||||
{:disabled disabled-position?
|
||||
:on-change on-pos-y-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/character-y
|
||||
:name :y
|
||||
:attr :y
|
||||
:align :right
|
||||
:property (tr "workspace.options.y")
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]]
|
||||
:applied-token (get applied-tokens :y)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :y)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :y)}]]
|
||||
|
||||
[:*
|
||||
[:div {:class (stl/css-case :x-position true
|
||||
@@ -579,10 +532,13 @@
|
||||
:icon i/rotation
|
||||
:min -359
|
||||
:max 359
|
||||
:name :rotation
|
||||
:attr :rotation
|
||||
:property (tr "workspace.options.rotation")
|
||||
:applied-tokens applied-tokens
|
||||
:values values}]
|
||||
:applied-token (get applied-tokens :rotation)
|
||||
:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple (get values :rotation)))
|
||||
(tr "settings.multiple") "--")
|
||||
:value (get values :rotation)}]
|
||||
|
||||
[:div {:class (stl/css :rotation)
|
||||
:title (tr "workspace.options.rotation")}
|
||||
|
||||
@@ -176,7 +176,8 @@
|
||||
:token token
|
||||
:shape-ids ids}))))]
|
||||
|
||||
[:div {:class (stl/css :stroke-section)}
|
||||
[:section {:class (stl/css :stroke-section)
|
||||
:aria-label "stroke-section"}
|
||||
[:div {:class (stl/css :stroke-title)}
|
||||
[:> title-bar* {:collapsable has-strokes?
|
||||
:collapsed (not open?)
|
||||
|
||||
@@ -10,13 +10,15 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.types.color :as ctc]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.reorder-handler :refer [reorder-handler*]]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.hooks :as h]
|
||||
[app.main.ui.workspace.sidebar.options.menus.input-wrapper-tokens :refer [numeric-input-wrapper*]]
|
||||
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -45,7 +47,10 @@
|
||||
select-on-focus
|
||||
ids]}]
|
||||
|
||||
(let [on-drop
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
on-drop
|
||||
(mf/use-fn
|
||||
(mf/deps on-reorder index)
|
||||
(fn [relative-pos data]
|
||||
@@ -88,7 +93,13 @@
|
||||
on-width-change
|
||||
(mf/use-fn
|
||||
(mf/deps index on-stroke-width-change)
|
||||
#(on-stroke-width-change index %))
|
||||
(fn [value]
|
||||
(if (or (string? value) (int? value))
|
||||
(on-stroke-width-change index value)
|
||||
(do
|
||||
(st/emit! (dwta/toggle-token {:token (first value)
|
||||
:attrs #{:stroke-width}
|
||||
:shape-ids ids}))))))
|
||||
|
||||
stroke-alignment (or (:stroke-alignment stroke) :center)
|
||||
|
||||
@@ -149,6 +160,12 @@
|
||||
(fn [token]
|
||||
(on-detach-token token #{:stroke-color})))
|
||||
|
||||
on-detach-token-width
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach-token)
|
||||
(fn [token]
|
||||
(on-detach-token (first token) #{:stroke-width})))
|
||||
|
||||
stroke-caps-options
|
||||
[{:value nil :label (tr "workspace.options.stroke-cap.none")}
|
||||
:separator
|
||||
@@ -169,7 +186,8 @@
|
||||
[:div {:class (stl/css-case
|
||||
:stroke-data true
|
||||
:dnd-over-top (= (:over dprops) :top)
|
||||
:dnd-over-bot (= (:over dprops) :bot))}
|
||||
:dnd-over-bot (= (:over dprops) :bot))
|
||||
:aria-label (str "stroke-row-" index)}
|
||||
|
||||
(when (some? on-reorder)
|
||||
[:> reorder-handler* {:ref dref}])
|
||||
@@ -195,17 +213,30 @@
|
||||
|
||||
;; Stroke Width, Alignment & Style
|
||||
[:div {:class (stl/css :stroke-options)}
|
||||
[:div {:class (stl/css :stroke-width-input)
|
||||
:title (tr "workspace.options.stroke-width")}
|
||||
[:> icon* {:icon-id i/stroke-size
|
||||
:size "s"}]
|
||||
[:> numeric-input* {:value stroke-width
|
||||
:min 0
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-change on-width-change
|
||||
:on-focus on-focus
|
||||
:select-on-focus select-on-focus
|
||||
:on-blur on-blur}]]
|
||||
(if token-numeric-inputs
|
||||
[:> numeric-input-wrapper* {:on-change on-width-change
|
||||
:on-detach on-detach-token-width
|
||||
:icon i/stroke-size
|
||||
:min 0
|
||||
:on-focus on-focus
|
||||
:on-blur on-blur
|
||||
:attr :stroke-width
|
||||
:class (stl/css :numeric-input-wrapper)
|
||||
:property (tr "workspace.options.stroke-width")
|
||||
:applied-token (get applied-tokens :stroke-width)
|
||||
:value stroke-width}]
|
||||
|
||||
[:div {:class (stl/css :stroke-width-input)
|
||||
:title (tr "workspace.options.stroke-width")}
|
||||
[:> icon* {:icon-id i/stroke-size
|
||||
:size "s"}]
|
||||
[:> deprecated-input/numeric-input* {:value stroke-width
|
||||
:min 0
|
||||
:placeholder (tr "settings.multiple")
|
||||
:on-change on-width-change
|
||||
:on-focus on-focus
|
||||
:select-on-focus select-on-focus
|
||||
:on-blur on-blur}]])
|
||||
|
||||
[:div {:class (stl/css :stroke-alignment-select)
|
||||
:data-testid "stroke.alignment"}
|
||||
|
||||
@@ -45,6 +45,11 @@
|
||||
padding-inline-start: var(--sp-xs);
|
||||
}
|
||||
|
||||
.numeric-input-wrapper {
|
||||
grid-column: span 2;
|
||||
--dropdown-width: var(--7-columns-dropdown-width);
|
||||
}
|
||||
|
||||
.stroke-alignment-select {
|
||||
grid-column: span 3;
|
||||
}
|
||||
|
||||
@@ -114,6 +114,7 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -135,6 +135,7 @@
|
||||
:is-flex-layout? is-flex-layout?
|
||||
:is-grid-layout? is-grid-layout?
|
||||
:is-layout-child? is-layout-child?
|
||||
:applied-tokens applied-tokens
|
||||
:is-layout-container? is-layout-container?
|
||||
:shape shape}])
|
||||
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:values layout-item-values}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -409,7 +409,7 @@
|
||||
[layout-container-ids layout-container-values layout-container-tokens]
|
||||
(get-attrs shapes objects :layout-container)
|
||||
|
||||
[layout-item-ids layout-item-values {}]
|
||||
[layout-item-ids layout-item-values layout-item-tokens]
|
||||
(get-attrs shapes objects :layout-item)
|
||||
|
||||
components
|
||||
@@ -471,6 +471,7 @@
|
||||
:is-layout-container? all-flex-layout-container?
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens layout-item-tokens
|
||||
:values layout-item-values}])
|
||||
|
||||
(when-not (or (empty? constraint-ids) ^boolean is-layout-child?)
|
||||
|
||||
@@ -113,6 +113,7 @@
|
||||
:is-layout-container? false
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -112,6 +112,7 @@
|
||||
:values layout-item-values
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:shape shape}])
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -154,6 +154,7 @@
|
||||
:is-layout-child? true
|
||||
:is-flex-parent? is-flex-parent?
|
||||
:is-grid-parent? is-grid-parent?
|
||||
:applied-tokens applied-tokens
|
||||
:shape shape}])
|
||||
|
||||
(when (or (not ^boolean is-layout-child?) ^boolean is-layout-child-absolute?)
|
||||
|
||||
@@ -223,7 +223,7 @@
|
||||
gap-items (all-or-separate-actions {:attribute-labels {:column-gap "Column Gap"
|
||||
:row-gap "Row Gap"}
|
||||
:hint (tr "workspace.tokens.gaps")
|
||||
:on-update-shape dwta/update-layout-spacing}
|
||||
:on-update-shape dwta/update-layout-gap}
|
||||
context-data)]
|
||||
(->> (concat
|
||||
gap-items
|
||||
@@ -239,7 +239,7 @@
|
||||
(all-or-separate-actions {:attribute-labels {:width "Width"
|
||||
:height "Height"}
|
||||
:hint (tr "workspace.tokens.size")
|
||||
:on-update-shape dwta/update-shape-dimensions}
|
||||
:on-update-shape dwta/apply-dimensions-token}
|
||||
context-data)
|
||||
[:separator]
|
||||
(all-or-separate-actions {:attribute-labels {:layout-item-min-w "Min Width"
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
[app.main.ui.shapes.text]
|
||||
[app.main.worker :as mw]
|
||||
[app.render-wasm.api.fonts :as f]
|
||||
[app.render-wasm.api.shapes :as shapes]
|
||||
[app.render-wasm.api.texts :as t]
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[app.render-wasm.deserializers :as dr]
|
||||
@@ -68,12 +69,25 @@
|
||||
(def ^:const DEBOUNCE_DELAY_MS 100)
|
||||
(def ^:const THROTTLE_DELAY_MS 10)
|
||||
|
||||
;; Number of shapes to process before yielding to browser
|
||||
(def ^:const SHAPES_CHUNK_SIZE 100)
|
||||
;; Threshold below which we use synchronous processing (no chunking overhead)
|
||||
(def ^:const ASYNC_THRESHOLD 100)
|
||||
|
||||
(def dpr
|
||||
(if use-dpr? (if (exists? js/window) js/window.devicePixelRatio 1.0) 1.0))
|
||||
|
||||
(def noop-fn
|
||||
(constantly nil))
|
||||
|
||||
(defn- yield-to-browser
|
||||
"Returns a promise that resolves after yielding to the browser's event loop.
|
||||
Uses requestAnimationFrame for smooth visual updates during loading."
|
||||
[]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(js/requestAnimationFrame (fn [_] (resolve nil))))))
|
||||
|
||||
;; Based on app.main.render/object-svg
|
||||
(mf/defc object-svg
|
||||
{::mf/props :obj}
|
||||
@@ -120,17 +134,56 @@
|
||||
(aget buffer 3))
|
||||
(set! wasm/internal-frame-id nil))))
|
||||
|
||||
(defn render-preview!
|
||||
"Render a lightweight preview without tile caching.
|
||||
Used during progressive loading for fast feedback."
|
||||
[]
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(h/call wasm/internal-module "_render_preview")))
|
||||
|
||||
|
||||
(defonce pending-render (atom false))
|
||||
(defonce shapes-loading? (atom false))
|
||||
(defonce deferred-render? (atom false))
|
||||
|
||||
(defn- register-deferred-render!
|
||||
[]
|
||||
(reset! deferred-render? true))
|
||||
|
||||
(defn request-render
|
||||
[_requester]
|
||||
(when (and wasm/context-initialized? (not @pending-render) (not @wasm/context-lost?))
|
||||
(reset! pending-render true)
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(render ts)))))
|
||||
(when (and wasm/context-initialized? (not @wasm/context-lost?))
|
||||
(if @shapes-loading?
|
||||
(register-deferred-render!)
|
||||
(when-not @pending-render
|
||||
(reset! pending-render true)
|
||||
(let [frame-id
|
||||
(js/requestAnimationFrame
|
||||
(fn [ts]
|
||||
(reset! pending-render false)
|
||||
(set! wasm/internal-frame-id nil)
|
||||
(render ts)))]
|
||||
(set! wasm/internal-frame-id frame-id))))))
|
||||
|
||||
(defn- begin-shapes-loading!
|
||||
[]
|
||||
(reset! shapes-loading? true)
|
||||
(let [frame-id wasm/internal-frame-id
|
||||
was-pending @pending-render]
|
||||
(when frame-id
|
||||
(js/cancelAnimationFrame frame-id)
|
||||
(set! wasm/internal-frame-id nil))
|
||||
(reset! pending-render false)
|
||||
(reset! deferred-render? was-pending)))
|
||||
|
||||
(defn- end-shapes-loading!
|
||||
[]
|
||||
(let [was-loading (compare-and-set! shapes-loading? true false)]
|
||||
(reset! deferred-render? false)
|
||||
;; Always trigger a render after loading completes
|
||||
;; This ensures shapes are displayed even if no deferred render was requested
|
||||
(when was-loading
|
||||
(request-render "set-objects:flush"))))
|
||||
|
||||
(declare get-text-dimensions)
|
||||
|
||||
@@ -895,24 +948,12 @@
|
||||
id (dm/get-prop shape :id)
|
||||
type (dm/get-prop shape :type)
|
||||
|
||||
parent-id (get shape :parent-id)
|
||||
masked (get shape :masked-group)
|
||||
selrect (get shape :selrect)
|
||||
constraint-h (get shape :constraints-h)
|
||||
constraint-v (get shape :constraints-v)
|
||||
clip-content (if (= type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
rotation (get shape :rotation)
|
||||
transform (get shape :transform)
|
||||
|
||||
fills (get shape :fills)
|
||||
strokes (if (= type :group)
|
||||
[] (get shape :strokes))
|
||||
children (get shape :shapes)
|
||||
blend-mode (get shape :blend-mode)
|
||||
opacity (get shape :opacity)
|
||||
hidden (get shape :hidden)
|
||||
content (let [content (get shape :content)]
|
||||
(if (= type :text)
|
||||
(ensure-text-content content)
|
||||
@@ -921,22 +962,12 @@
|
||||
grow-type (get shape :grow-type)
|
||||
blur (get shape :blur)
|
||||
svg-attrs (get shape :svg-attrs)
|
||||
shadows (get shape :shadow)
|
||||
corners (map #(get shape %) [:r1 :r2 :r3 :r4])]
|
||||
shadows (get shape :shadow)]
|
||||
|
||||
(use-shape id)
|
||||
(set-parent-id parent-id)
|
||||
(set-shape-type type)
|
||||
(set-shape-clip-content clip-content)
|
||||
(set-shape-constraints constraint-h constraint-v)
|
||||
(shapes/set-shape-base-props shape)
|
||||
|
||||
(set-shape-rotation rotation)
|
||||
(set-shape-transform transform)
|
||||
(set-shape-blend-mode blend-mode)
|
||||
(set-shape-opacity opacity)
|
||||
(set-shape-hidden hidden)
|
||||
;; Remaining properties that need separate calls (variable-length or conditional)
|
||||
(set-shape-children children)
|
||||
(set-shape-corners corners)
|
||||
(set-shape-blur blur)
|
||||
(when (= type :group)
|
||||
(set-masked (boolean masked)))
|
||||
@@ -956,7 +987,6 @@
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(set-shape-selrect selrect)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
(set-shape-text-content id content)
|
||||
@@ -1012,30 +1042,143 @@
|
||||
(let [{:keys [thumbnails full]} (set-object shape)]
|
||||
(process-pending [shape] thumbnails full noop-fn)))
|
||||
|
||||
(defn- process-shapes-chunk
|
||||
"Process a chunk of shapes synchronously, returning accumulated pending operations.
|
||||
Returns {:thumbnails [...] :full [...] :next-index n}"
|
||||
[shapes start-index chunk-size thumbnails-acc full-acc]
|
||||
(let [total (count shapes)
|
||||
end-index (min total (+ start-index chunk-size))]
|
||||
(loop [index start-index
|
||||
t-acc thumbnails-acc
|
||||
f-acc full-acc]
|
||||
(if (< index end-index)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into t-acc thumbnails)
|
||||
(into f-acc full)))
|
||||
{:thumbnails t-acc
|
||||
:full f-acc
|
||||
:next-index end-index}))))
|
||||
|
||||
(defn- set-objects-async
|
||||
"Asynchronously process shapes in chunks, yielding to the browser between chunks.
|
||||
Returns a promise that resolves when all shapes are processed.
|
||||
|
||||
Renders a preview only periodically during loading to show progress,
|
||||
then does a full tile-based render at the end."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
total-chunks (mth/ceil (/ total-shapes SHAPES_CHUNK_SIZE))
|
||||
;; Render at 25%, 50%, 75% of loading
|
||||
render-at-chunks (set [(mth/floor (* total-chunks 0.25))
|
||||
(mth/floor (* total-chunks 0.5))
|
||||
(mth/floor (* total-chunks 0.75))])]
|
||||
(p/create
|
||||
(fn [resolve _reject]
|
||||
(letfn [(process-next-chunk [index thumbnails-acc full-acc chunk-count]
|
||||
(if (< index total-shapes)
|
||||
;; Process one chunk
|
||||
(let [{:keys [thumbnails full next-index]}
|
||||
(process-shapes-chunk shapes index SHAPES_CHUNK_SIZE
|
||||
thumbnails-acc full-acc)
|
||||
new-chunk-count (inc chunk-count)]
|
||||
;; Only render at specific progress milestones
|
||||
(when (contains? render-at-chunks new-chunk-count)
|
||||
(render-preview!))
|
||||
|
||||
;; Yield to browser, then continue with next chunk
|
||||
(-> (yield-to-browser)
|
||||
(p/then (fn [_]
|
||||
(process-next-chunk next-index thumbnails full new-chunk-count)))))
|
||||
;; All chunks done - finalize
|
||||
(do
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails-acc full-acc noop-fn
|
||||
(fn []
|
||||
(end-shapes-loading!)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))
|
||||
(resolve nil))))))]
|
||||
(process-next-chunk 0 [] [] 0))))))
|
||||
|
||||
(defn- set-objects-sync
|
||||
"Synchronously process all shapes (for small shape counts)."
|
||||
[shapes render-callback]
|
||||
(let [total-shapes (count shapes)
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))
|
||||
|
||||
(defn- shapes-in-tree-order
|
||||
"Returns shapes sorted in tree order (parents before children).
|
||||
This ensures parent shapes are processed before their children,
|
||||
maintaining proper shape reference consistency in WASM."
|
||||
[objects]
|
||||
;; Get IDs in tree order starting from root (uuid/zero)
|
||||
;; If root doesn't exist (e.g., filtered thumbnail data), fall back to
|
||||
;; finding top-level shapes (those without a parent in objects) and
|
||||
;; traversing from there.
|
||||
(if (contains? objects uuid/zero)
|
||||
;; Normal case: traverse from root
|
||||
(let [ordered-ids (cfh/get-children-ids-with-self objects uuid/zero)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
ordered-ids))
|
||||
;; Fallback for filtered data (thumbnails): find top-level shapes and traverse
|
||||
(let [;; Find shapes whose parent is not in the objects map (top-level in this subset)
|
||||
top-level-ids (->> (vals objects)
|
||||
(filter (fn [shape]
|
||||
(not (contains? objects (:parent-id shape)))))
|
||||
(map :id))
|
||||
;; Get all children in order for each top-level shape
|
||||
all-ordered-ids (into []
|
||||
(mapcat #(cfh/get-children-ids-with-self objects %))
|
||||
top-level-ids)]
|
||||
(into []
|
||||
(keep #(get objects %))
|
||||
all-ordered-ids))))
|
||||
|
||||
(defn set-objects
|
||||
"Set all shape objects for rendering.
|
||||
|
||||
Shapes are processed in tree order (parents before children)
|
||||
to maintain proper shape reference consistency in WASM."
|
||||
([objects]
|
||||
(set-objects objects nil))
|
||||
([objects render-callback]
|
||||
(perf/begin-measure "set-objects")
|
||||
(let [shapes (into [] (vals objects))
|
||||
total-shapes (count shapes)
|
||||
;; Collect pending operations - set-object returns {:thumbnails [...] :full [...]}
|
||||
{:keys [thumbnails full]}
|
||||
(loop [index 0 thumbnails-acc [] full-acc []]
|
||||
(if (< index total-shapes)
|
||||
(let [shape (nth shapes index)
|
||||
{:keys [thumbnails full]} (set-object shape)]
|
||||
(recur (inc index)
|
||||
(into thumbnails-acc thumbnails)
|
||||
(into full-acc full)))
|
||||
{:thumbnails thumbnails-acc :full full-acc}))]
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
(let [shapes (shapes-in-tree-order objects)
|
||||
total-shapes (count shapes)]
|
||||
(if (< total-shapes ASYNC_THRESHOLD)
|
||||
(set-objects-sync shapes render-callback)
|
||||
(do
|
||||
(begin-shapes-loading!)
|
||||
(try
|
||||
(-> (set-objects-async shapes render-callback)
|
||||
(p/catch (fn [error]
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error))))
|
||||
(catch :default error
|
||||
(end-shapes-loading!)
|
||||
(js/console.error "Async WASM shape loading failed" error)
|
||||
(throw error)))
|
||||
nil)))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
[]
|
||||
|
||||
193
frontend/src/app/render_wasm/api/shapes.cljs
Normal file
193
frontend/src/app/render_wasm/api/shapes.cljs
Normal file
@@ -0,0 +1,193 @@
|
||||
;; This Source Code Form is subject to the terms of the Mozilla Public
|
||||
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
;;
|
||||
;; Copyright (c) KALEIDOS INC
|
||||
|
||||
(ns app.render-wasm.api.shapes
|
||||
"Batched shape property serialization for improved WASM performance.
|
||||
|
||||
This module provides a single WASM call to set all base shape properties,
|
||||
replacing multiple individual calls (use_shape, set_parent, set_shape_type,
|
||||
etc.) with one batched operation."
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.wasm :as wasm]))
|
||||
|
||||
;; Binary layout constants matching Rust implementation:
|
||||
;;
|
||||
;; | Offset | Size | Field | Type |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | 0 | 16 | id | UUID (4 × u32 LE) |
|
||||
;; | 16 | 16 | parent_id | UUID (4 × u32 LE) |
|
||||
;; | 32 | 1 | shape_type | u8 |
|
||||
;; | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
|
||||
;; | 34 | 1 | blend_mode | u8 |
|
||||
;; | 35 | 1 | constraint_h | u8 (0xFF = None) |
|
||||
;; | 36 | 1 | constraint_v | u8 (0xFF = None) |
|
||||
;; | 37 | 3 | padding | - |
|
||||
;; | 40 | 4 | opacity | f32 LE |
|
||||
;; | 44 | 4 | rotation | f32 LE |
|
||||
;; | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
|
||||
;; | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
|
||||
;; | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
|
||||
;; |--------|------|--------------|-----------------------------------|
|
||||
;; | Total | 104 | | |
|
||||
|
||||
(def ^:const BASE-PROPS-SIZE 104)
|
||||
(def ^:const FLAG-CLIP-CONTENT 0x01)
|
||||
(def ^:const FLAG-HIDDEN 0x02)
|
||||
(def ^:const CONSTRAINT-NONE 0xFF)
|
||||
|
||||
(defn- write-uuid-to-heap
|
||||
"Write a UUID to the heap at the given byte offset using DataView."
|
||||
[dview offset id]
|
||||
(let [buffer (uuid/get-u32 id)]
|
||||
(.setUint32 dview offset (aget buffer 0) true)
|
||||
(.setUint32 dview (+ offset 4) (aget buffer 1) true)
|
||||
(.setUint32 dview (+ offset 8) (aget buffer 2) true)
|
||||
(.setUint32 dview (+ offset 12) (aget buffer 3) true)))
|
||||
|
||||
(defn- serialize-transform
|
||||
"Extract transform matrix values, defaulting to identity matrix."
|
||||
[transform]
|
||||
(if (some? transform)
|
||||
[(dm/get-prop transform :a)
|
||||
(dm/get-prop transform :b)
|
||||
(dm/get-prop transform :c)
|
||||
(dm/get-prop transform :d)
|
||||
(dm/get-prop transform :e)
|
||||
(dm/get-prop transform :f)]
|
||||
[1.0 0.0 0.0 1.0 0.0 0.0])) ; identity matrix
|
||||
|
||||
(defn- serialize-selrect
|
||||
"Extract selrect values."
|
||||
[selrect]
|
||||
(if (some? selrect)
|
||||
[(dm/get-prop selrect :x1)
|
||||
(dm/get-prop selrect :y1)
|
||||
(dm/get-prop selrect :x2)
|
||||
(dm/get-prop selrect :y2)]
|
||||
[0.0 0.0 0.0 0.0]))
|
||||
|
||||
(defn set-shape-base-props
|
||||
"Set all base shape properties in a single WASM call.
|
||||
|
||||
This replaces the following individual calls:
|
||||
- use-shape
|
||||
- set-parent-id
|
||||
- set-shape-type
|
||||
- set-shape-clip-content
|
||||
- set-shape-rotation
|
||||
- set-shape-transform
|
||||
- set-shape-blend-mode
|
||||
- set-shape-opacity
|
||||
- set-shape-hidden
|
||||
- set-shape-selrect
|
||||
- set-shape-corners
|
||||
- set-shape-constraints (clear + h + v)
|
||||
|
||||
Returns nil."
|
||||
[shape]
|
||||
(when wasm/context-initialized?
|
||||
(let [id (dm/get-prop shape :id)
|
||||
parent-id (get shape :parent-id)
|
||||
shape-type (dm/get-prop shape :type)
|
||||
|
||||
clip-content (if (= shape-type :frame)
|
||||
(not (get shape :show-content))
|
||||
false)
|
||||
hidden (get shape :hidden false)
|
||||
|
||||
flags (cond-> 0
|
||||
clip-content (bit-or FLAG-CLIP-CONTENT)
|
||||
hidden (bit-or FLAG-HIDDEN))
|
||||
|
||||
blend-mode (sr/translate-blend-mode (get shape :blend-mode))
|
||||
constraint-h (let [c (get shape :constraints-h)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-h c)
|
||||
CONSTRAINT-NONE))
|
||||
constraint-v (let [c (get shape :constraints-v)]
|
||||
(if (some? c)
|
||||
(sr/translate-constraint-v c)
|
||||
CONSTRAINT-NONE))
|
||||
|
||||
opacity (d/nilv (get shape :opacity) 1.0)
|
||||
rotation (d/nilv (get shape :rotation) 0.0)
|
||||
|
||||
;; Transform matrix
|
||||
[ta tb tc td te tf] (serialize-transform (get shape :transform))
|
||||
|
||||
;; Selrect
|
||||
selrect (get shape :selrect)
|
||||
[sx1 sy1 sx2 sy2] (serialize-selrect selrect)
|
||||
|
||||
;; Corners
|
||||
r1 (d/nilv (get shape :r1) 0.0)
|
||||
r2 (d/nilv (get shape :r2) 0.0)
|
||||
r3 (d/nilv (get shape :r3) 0.0)
|
||||
r4 (d/nilv (get shape :r4) 0.0)
|
||||
|
||||
;; Allocate buffer and get DataView
|
||||
offset (mem/alloc BASE-PROPS-SIZE)
|
||||
heap (mem/get-heap-u8)
|
||||
dview (js/DataView. (.-buffer heap))]
|
||||
|
||||
;; Write id (offset 0, 16 bytes)
|
||||
(write-uuid-to-heap dview offset id)
|
||||
|
||||
;; Write parent_id (offset 16, 16 bytes)
|
||||
(write-uuid-to-heap dview (+ offset 16) (d/nilv parent-id uuid/zero))
|
||||
|
||||
;; Write shape_type (offset 32, 1 byte)
|
||||
(.setUint8 dview (+ offset 32) (sr/translate-shape-type shape-type))
|
||||
|
||||
;; Write flags (offset 33, 1 byte)
|
||||
(.setUint8 dview (+ offset 33) flags)
|
||||
|
||||
;; Write blend_mode (offset 34, 1 byte)
|
||||
(.setUint8 dview (+ offset 34) blend-mode)
|
||||
|
||||
;; Write constraint_h (offset 35, 1 byte)
|
||||
(.setUint8 dview (+ offset 35) constraint-h)
|
||||
|
||||
;; Write constraint_v (offset 36, 1 byte)
|
||||
(.setUint8 dview (+ offset 36) constraint-v)
|
||||
|
||||
;; Padding at offset 37-39 (already zero from alloc)
|
||||
|
||||
;; Write opacity (offset 40, f32)
|
||||
(.setFloat32 dview (+ offset 40) opacity true)
|
||||
|
||||
;; Write rotation (offset 44, f32)
|
||||
(.setFloat32 dview (+ offset 44) rotation true)
|
||||
|
||||
;; Write transform matrix (offset 48, 6 × f32)
|
||||
(.setFloat32 dview (+ offset 48) ta true)
|
||||
(.setFloat32 dview (+ offset 52) tb true)
|
||||
(.setFloat32 dview (+ offset 56) tc true)
|
||||
(.setFloat32 dview (+ offset 60) td true)
|
||||
(.setFloat32 dview (+ offset 64) te true)
|
||||
(.setFloat32 dview (+ offset 68) tf true)
|
||||
|
||||
;; Write selrect (offset 72, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 72) sx1 true)
|
||||
(.setFloat32 dview (+ offset 76) sy1 true)
|
||||
(.setFloat32 dview (+ offset 80) sx2 true)
|
||||
(.setFloat32 dview (+ offset 84) sy2 true)
|
||||
|
||||
;; Write corners (offset 88, 4 × f32)
|
||||
(.setFloat32 dview (+ offset 88) r1 true)
|
||||
(.setFloat32 dview (+ offset 92) r2 true)
|
||||
(.setFloat32 dview (+ offset 96) r3 true)
|
||||
(.setFloat32 dview (+ offset 100) r4 true)
|
||||
|
||||
(h/call wasm/internal-module "_set_shape_base_props")
|
||||
|
||||
nil)))
|
||||
@@ -802,9 +802,10 @@
|
||||
([uri name]
|
||||
(open-new-window uri name "noopener,noreferrer"))
|
||||
([uri name features]
|
||||
(let [new-window (.open js/window (str uri) name features)]
|
||||
(when-let [new-window (.open js/window (str uri) name features)]
|
||||
(when (not= name "_blank")
|
||||
(.reload (.-location new-window))))))
|
||||
(when-let [location (.-location new-window)]
|
||||
(.reload location))))))
|
||||
|
||||
(defn browser-back
|
||||
[]
|
||||
|
||||
@@ -260,7 +260,7 @@
|
||||
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:width :height}
|
||||
:token (toht/get-token file "dimensions.sm")
|
||||
:on-update-shape dwta/update-shape-dimensions})]]
|
||||
:on-update-shape dwta/apply-dimensions-token})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
@@ -333,7 +333,7 @@
|
||||
events [(dwta/apply-token {:shape-ids [(:id rect-1)]
|
||||
:attributes #{:width :height}
|
||||
:token (toht/get-token file "sizing.sm")
|
||||
:on-update-shape dwta/update-shape-dimensions})]]
|
||||
:on-update-shape dwta/apply-dimensions-token})]]
|
||||
(tohs/run-store-async
|
||||
store done events
|
||||
(fn [new-state]
|
||||
|
||||
@@ -6713,19 +6713,19 @@ msgstr "Advanced options"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:543
|
||||
msgid "workspace.options.layout-item.layout-item-max-h"
|
||||
msgstr "Max.Height"
|
||||
msgstr "Max height"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:510
|
||||
msgid "workspace.options.layout-item.layout-item-max-w"
|
||||
msgstr "Max.Width"
|
||||
msgstr "Max width"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:527
|
||||
msgid "workspace.options.layout-item.layout-item-min-h"
|
||||
msgstr "Min.Height"
|
||||
msgstr "Min height"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs:494
|
||||
msgid "workspace.options.layout-item.layout-item-min-w"
|
||||
msgstr "Min.Width"
|
||||
msgstr "Min width"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/layout_item.cljs
|
||||
#, unused
|
||||
|
||||
@@ -7164,11 +7164,11 @@ msgstr "Ancho"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:535, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:552
|
||||
msgid "workspace.options.x"
|
||||
msgstr "eje X"
|
||||
msgstr "Eje X"
|
||||
|
||||
#: src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:545, src/app/main/ui/workspace/sidebar/options/menus/measures.cljs:563
|
||||
msgid "workspace.options.y"
|
||||
msgstr "eje Y"
|
||||
msgstr "Eje Y"
|
||||
|
||||
#: src/app/main/ui/workspace/viewport/path_actions.cljs:140
|
||||
msgid "workspace.path.actions.add-node"
|
||||
|
||||
@@ -124,7 +124,7 @@ function run-devenv-shell {
|
||||
docker exec -ti \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-e EXTERNAL_UID=$CURRENT_USER_ID \
|
||||
penpot-devenv-main sudo -EH -u penpot bash;
|
||||
penpot-devenv-main sudo -EH -u penpot $@
|
||||
}
|
||||
|
||||
function run-devenv-isolated-shell {
|
||||
@@ -138,7 +138,7 @@ function run-devenv-isolated-shell {
|
||||
-e SHADOWCLJS_EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS \
|
||||
-e JAVA_OPTS="$JAVA_OPTS" \
|
||||
-w /home/penpot/penpot/$1 \
|
||||
$DEVENV_IMGNAME:latest sudo -EH -u penpot bash
|
||||
$DEVENV_IMGNAME:latest sudo -EH -u penpot $@
|
||||
}
|
||||
|
||||
function build-imagemagick-docker-image {
|
||||
|
||||
8
plugins/apps/colors-to-tokens-plugin/wrangler.toml
Normal file
8
plugins/apps/colors-to-tokens-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "color-to-tokens-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/colors-to-tokens-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/contrast-plugin/wrangler.toml
Normal file
8
plugins/apps/contrast-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "contrast-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/contrast-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/create-palette-plugin/wrangler.toml
Normal file
8
plugins/apps/create-palette-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "create-palette-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/create-palette-plugin" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/icons-plugin/wrangler.toml
Normal file
8
plugins/apps/icons-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "icons-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/icons-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/lorem-ipsum-plugin/wrangler.toml
Normal file
8
plugins/apps/lorem-ipsum-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "lorem-ipsum-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/lorem-ipsum-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/rename-layers-plugin/wrangler.toml
Normal file
8
plugins/apps/rename-layers-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "rename-layers-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/rename-layers-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
8
plugins/apps/table-plugin/wrangler.toml
Normal file
8
plugins/apps/table-plugin/wrangler.toml
Normal file
@@ -0,0 +1,8 @@
|
||||
name = "table-plugin"
|
||||
compatibility_date = "2025-01-01"
|
||||
|
||||
assets = { directory = "../../dist/apps/table-plugin/browser" }
|
||||
|
||||
[[routes]]
|
||||
pattern = "WORKER_URI"
|
||||
custom_domain = true
|
||||
@@ -23,7 +23,7 @@ use std::collections::HashMap;
|
||||
use utils::uuid_from_u32_quartet;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub(crate) static mut STATE: Option<Box<State<'static>>> = None;
|
||||
pub(crate) static mut STATE: Option<Box<State>> = None;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! with_state_mut {
|
||||
@@ -191,6 +191,20 @@ pub extern "C" fn render_from_cache(_: i32) {
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_preview_mode(enabled: bool) {
|
||||
with_state_mut!(state, {
|
||||
state.render_state.set_preview_mode(enabled);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn render_preview() {
|
||||
with_state_mut!(state, {
|
||||
state.render_preview(performance::get_time());
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn process_animation_frame(timestamp: i32) {
|
||||
let result = std::panic::catch_unwind(|| {
|
||||
|
||||
@@ -294,6 +294,8 @@ pub(crate) struct RenderState {
|
||||
/// where we must render shapes without inheriting ancestor layer blurs. Toggle it through
|
||||
/// `with_nested_blurs_suppressed` to ensure it's always restored.
|
||||
pub ignore_nested_blurs: bool,
|
||||
/// Preview render mode - when true, uses simplified rendering for progressive loading
|
||||
pub preview_mode: bool,
|
||||
}
|
||||
|
||||
pub fn get_cache_size(viewbox: Viewbox, scale: f32) -> skia::ISize {
|
||||
@@ -366,6 +368,7 @@ impl RenderState {
|
||||
focus_mode: FocusMode::new(),
|
||||
touched_ids: HashSet::default(),
|
||||
ignore_nested_blurs: false,
|
||||
preview_mode: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -486,6 +489,10 @@ impl RenderState {
|
||||
self.background_color = color;
|
||||
}
|
||||
|
||||
pub fn set_preview_mode(&mut self, enabled: bool) {
|
||||
self.preview_mode = enabled;
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
let dpr_width = (width as f32 * self.options.dpr()).floor() as i32;
|
||||
let dpr_height = (height as f32 * self.options.dpr()).floor() as i32;
|
||||
@@ -1127,6 +1134,25 @@ impl RenderState {
|
||||
performance::end_timed_log!("render_from_cache", _start);
|
||||
}
|
||||
|
||||
/// Render a preview of the shapes during loading.
|
||||
/// This rebuilds tiles for touched shapes and renders synchronously.
|
||||
pub fn render_preview(&mut self, tree: ShapesPoolRef, timestamp: i32) -> Result<(), String> {
|
||||
let _start = performance::begin_timed_log!("render_preview");
|
||||
performance::begin_measure!("render_preview");
|
||||
|
||||
// Skip tile rebuilding during preview - we'll do it at the end
|
||||
// Just rebuild tiles for touched shapes and render synchronously
|
||||
self.rebuild_touched_tiles(tree);
|
||||
|
||||
// Use the sync render path
|
||||
self.start_render_loop(None, tree, timestamp, true)?;
|
||||
|
||||
performance::end_measure!("render_preview");
|
||||
performance::end_timed_log!("render_preview", _start);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn start_render_loop(
|
||||
&mut self,
|
||||
base_object: Option<&Uuid>,
|
||||
@@ -1622,10 +1648,11 @@ impl RenderState {
|
||||
|
||||
is_empty = false;
|
||||
|
||||
let element = tree.get(&node_id).ok_or(format!(
|
||||
"Error: Element with root_id {} not found in the tree.",
|
||||
node_render_state.id
|
||||
))?;
|
||||
let Some(element) = tree.get(&node_id) else {
|
||||
// The shape isn't available yet (likely still streaming in from WASM).
|
||||
// Skip it for this pass; a subsequent render will pick it up once present.
|
||||
continue;
|
||||
};
|
||||
let scale = self.get_scale();
|
||||
let mut extrect: Option<Rect> = None;
|
||||
|
||||
@@ -1743,7 +1770,9 @@ impl RenderState {
|
||||
if !matches!(element.shape_type, Type::Bool(_)) {
|
||||
// Nested shapes shadowing - apply black shadow to child shapes too
|
||||
for shadow_shape_id in element.children.iter() {
|
||||
let shadow_shape = tree.get(shadow_shape_id).unwrap();
|
||||
let Some(shadow_shape) = tree.get(shadow_shape_id) else {
|
||||
continue;
|
||||
};
|
||||
if shadow_shape.hidden {
|
||||
continue;
|
||||
}
|
||||
@@ -2141,9 +2170,7 @@ impl RenderState {
|
||||
}
|
||||
|
||||
pub fn remove_cached_tile(&mut self, tile: tiles::Tile) {
|
||||
let rect = self.get_aligned_tile_bounds(tile);
|
||||
self.surfaces
|
||||
.remove_cached_tile_surface(tile, rect, self.background_color);
|
||||
self.surfaces.remove_cached_tile_surface(tile);
|
||||
}
|
||||
|
||||
pub fn rebuild_tiles_shallow(&mut self, tree: ShapesPoolRef) {
|
||||
@@ -2164,7 +2191,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the changed tiles
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
@@ -2211,7 +2238,7 @@ impl RenderState {
|
||||
}
|
||||
}
|
||||
|
||||
// Update the changed tiles
|
||||
// Invalidate changed tiles - old content stays visible until new tiles render
|
||||
self.surfaces.remove_cached_tiles(self.background_color);
|
||||
for tile in all_tiles {
|
||||
self.remove_cached_tile(tile);
|
||||
|
||||
@@ -401,11 +401,10 @@ impl Surfaces {
|
||||
self.tiles.has(tile)
|
||||
}
|
||||
|
||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile, rect: skia::Rect, color: skia::Color) {
|
||||
// Clear the specific tile area in the cache surface with color
|
||||
let mut paint = skia::Paint::default();
|
||||
paint.set_color(color);
|
||||
self.cache.canvas().draw_rect(rect, &paint);
|
||||
pub fn remove_cached_tile_surface(&mut self, tile: Tile) {
|
||||
// Mark tile as invalid
|
||||
// Old content stays visible until new tile overwrites it atomically,
|
||||
// preventing flickering during tile re-renders.
|
||||
self.tiles.remove(tile);
|
||||
}
|
||||
|
||||
|
||||
@@ -18,16 +18,16 @@ use crate::shapes::modifiers::grid_layout::grid_cell_data;
|
||||
/// It is created by [init] and passed to the other exported functions.
|
||||
/// Note that rust-skia data structures are not thread safe, so a state
|
||||
/// must not be shared between different Web Workers.
|
||||
pub(crate) struct State<'a> {
|
||||
pub(crate) struct State {
|
||||
pub render_state: RenderState,
|
||||
pub text_editor_state: TextEditorState,
|
||||
pub current_id: Option<Uuid>,
|
||||
pub current_browser: u8,
|
||||
pub shapes: ShapesPool<'a>,
|
||||
pub saved_shapes: Option<ShapesPool<'a>>,
|
||||
pub shapes: ShapesPool,
|
||||
pub saved_shapes: Option<ShapesPool>,
|
||||
}
|
||||
|
||||
impl<'a> State<'a> {
|
||||
impl State {
|
||||
pub fn new(width: i32, height: i32) -> Self {
|
||||
State {
|
||||
render_state: RenderState::new(width, height),
|
||||
@@ -223,17 +223,14 @@ impl<'a> State<'a> {
|
||||
self.render_state.rebuild_touched_tiles(&self.shapes);
|
||||
}
|
||||
|
||||
pub fn render_preview(&mut self, timestamp: i32) {
|
||||
let _ = self.render_state.render_preview(&self.shapes, timestamp);
|
||||
}
|
||||
|
||||
pub fn rebuild_modifier_tiles(&mut self, ids: Vec<Uuid>) {
|
||||
// SAFETY: We're extending the lifetime of the mutable borrow to 'a.
|
||||
// This is safe because:
|
||||
// 1. shapes has lifetime 'a in the struct
|
||||
// 2. The reference won't outlive the struct
|
||||
// 3. No other references to shapes exist during this call
|
||||
unsafe {
|
||||
let shapes_ptr = &mut self.shapes as *mut ShapesPool<'a>;
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut *shapes_ptr, ids);
|
||||
}
|
||||
// Index-based storage is safe
|
||||
self.render_state
|
||||
.rebuild_modifier_tiles(&mut self.shapes, ids);
|
||||
}
|
||||
|
||||
pub fn font_collection(&self) -> &FontCollection {
|
||||
|
||||
@@ -28,29 +28,44 @@ const SHAPES_POOL_ALLOC_MULTIPLIER: f32 = 1.3;
|
||||
/// Shapes are stored in a `Vec<Shape>`, which keeps the `Shape` instances
|
||||
/// in a contiguous memory block.
|
||||
///
|
||||
pub struct ShapesPoolImpl<'a> {
|
||||
/// # Index-based Design
|
||||
///
|
||||
/// All auxiliary HashMaps (modifiers, structure, scale_content, modified_shape_cache)
|
||||
/// use `usize` indices instead of `&'a Uuid` references. This eliminates:
|
||||
/// - Unsafe lifetime extensions
|
||||
/// - The need for `rebuild_references()` after Vec reallocation
|
||||
/// - Complex lifetime annotations
|
||||
///
|
||||
/// The `uuid_to_idx` HashMap maps `Uuid` (owned) to indices, avoiding lifetime issues.
|
||||
///
|
||||
pub struct ShapesPoolImpl {
|
||||
shapes: Vec<Shape>,
|
||||
counter: usize,
|
||||
|
||||
shapes_uuid_to_idx: HashMap<&'a Uuid, usize>,
|
||||
/// Maps UUID to index in the shapes Vec. Uses owned Uuid, no lifetime needed.
|
||||
uuid_to_idx: HashMap<Uuid, usize>,
|
||||
|
||||
modified_shape_cache: HashMap<&'a Uuid, OnceCell<Shape>>,
|
||||
modifiers: HashMap<&'a Uuid, skia::Matrix>,
|
||||
structure: HashMap<&'a Uuid, Vec<StructureEntry>>,
|
||||
scale_content: HashMap<&'a Uuid, f32>,
|
||||
/// Cache for modified shapes, keyed by index
|
||||
modified_shape_cache: HashMap<usize, OnceCell<Shape>>,
|
||||
/// Transform modifiers, keyed by index
|
||||
modifiers: HashMap<usize, skia::Matrix>,
|
||||
/// Structure entries, keyed by index
|
||||
structure: HashMap<usize, Vec<StructureEntry>>,
|
||||
/// Scale content values, keyed by index
|
||||
scale_content: HashMap<usize, f32>,
|
||||
}
|
||||
|
||||
// Type aliases to avoid writing lifetimes everywhere
|
||||
pub type ShapesPool<'a> = ShapesPoolImpl<'a>;
|
||||
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl<'a>;
|
||||
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl<'a>;
|
||||
// Type aliases - no longer need lifetimes!
|
||||
pub type ShapesPool = ShapesPoolImpl;
|
||||
pub type ShapesPoolRef<'a> = &'a ShapesPoolImpl;
|
||||
pub type ShapesPoolMutRef<'a> = &'a mut ShapesPoolImpl;
|
||||
|
||||
impl<'a> ShapesPoolImpl<'a> {
|
||||
impl ShapesPoolImpl {
|
||||
pub fn new() -> Self {
|
||||
ShapesPoolImpl {
|
||||
shapes: vec![],
|
||||
counter: 0,
|
||||
shapes_uuid_to_idx: HashMap::default(),
|
||||
uuid_to_idx: HashMap::default(),
|
||||
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
@@ -62,15 +77,14 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
pub fn initialize(&mut self, capacity: usize) {
|
||||
performance::begin_measure!("shapes_pool_initialize");
|
||||
self.counter = 0;
|
||||
self.shapes_uuid_to_idx = HashMap::with_capacity(capacity);
|
||||
self.uuid_to_idx = HashMap::with_capacity(capacity);
|
||||
|
||||
let additional = capacity as i32 - self.shapes.len() as i32;
|
||||
if additional <= 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Reserve exact capacity to avoid any future reallocations
|
||||
// This is critical because we store &'a Uuid references that would be invalidated
|
||||
// Reserve extra capacity to avoid future reallocations
|
||||
let target_capacity = (capacity as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
|
||||
self.shapes
|
||||
.reserve_exact(target_capacity.saturating_sub(self.shapes.len()));
|
||||
@@ -81,15 +95,15 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
pub fn add_shape(&mut self, id: Uuid) -> &mut Shape {
|
||||
let did_reallocate = if self.counter >= self.shapes.len() {
|
||||
// We need more space. Check if we'll need to reallocate the Vec.
|
||||
if self.counter >= self.shapes.len() {
|
||||
// We need more space
|
||||
let current_capacity = self.shapes.capacity();
|
||||
let additional = (self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize;
|
||||
// Ensure we add at least 1 shape when the pool is empty
|
||||
let additional =
|
||||
((self.shapes.len() as f32 * SHAPES_POOL_ALLOC_MULTIPLIER) as usize).max(1);
|
||||
let needed_capacity = self.shapes.len() + additional;
|
||||
|
||||
let will_reallocate = needed_capacity > current_capacity;
|
||||
|
||||
if will_reallocate {
|
||||
if needed_capacity > current_capacity {
|
||||
// Reserve extra space to minimize future reallocations
|
||||
let extra_reserve = (needed_capacity as f32 * 0.5) as usize;
|
||||
self.shapes
|
||||
@@ -98,165 +112,68 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
|
||||
self.shapes
|
||||
.extend(iter::repeat_with(|| Shape::new(Uuid::nil())).take(additional));
|
||||
|
||||
will_reallocate
|
||||
} else {
|
||||
false
|
||||
};
|
||||
}
|
||||
|
||||
let idx = self.counter;
|
||||
let new_shape = &mut self.shapes[idx];
|
||||
new_shape.id = id;
|
||||
|
||||
// Get a reference to the id field in the shape with lifetime 'a
|
||||
// SAFETY: This is safe because:
|
||||
// 1. We pre-allocate enough capacity to avoid Vec reallocation
|
||||
// 2. The shape and its id field won't move within the Vec
|
||||
// 3. The reference won't outlive the ShapesPoolImpl
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
|
||||
self.shapes_uuid_to_idx.insert(id_ref, idx);
|
||||
// Simply store the UUID -> index mapping. No unsafe lifetime tricks needed!
|
||||
self.uuid_to_idx.insert(id, idx);
|
||||
self.counter += 1;
|
||||
|
||||
// If the Vec reallocated, we need to rebuild all references in the HashMaps
|
||||
// because the old references point to deallocated memory
|
||||
if did_reallocate {
|
||||
self.rebuild_references();
|
||||
}
|
||||
|
||||
&mut self.shapes[idx]
|
||||
}
|
||||
|
||||
/// Rebuilds all &'a Uuid references in the HashMaps after a Vec reallocation.
|
||||
/// This is necessary because Vec reallocation invalidates all existing references.
|
||||
fn rebuild_references(&mut self) {
|
||||
// Rebuild shapes_uuid_to_idx with fresh references
|
||||
let mut new_map = HashMap::with_capacity(self.shapes_uuid_to_idx.len());
|
||||
for (_, idx) in self.shapes_uuid_to_idx.drain() {
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
new_map.insert(id_ref, idx);
|
||||
}
|
||||
self.shapes_uuid_to_idx = new_map;
|
||||
|
||||
// Rebuild modifiers with fresh references
|
||||
if !self.modifiers.is_empty() {
|
||||
let old_modifiers: Vec<(Uuid, skia::Matrix)> = self
|
||||
.modifiers
|
||||
.drain()
|
||||
.map(|(uuid_ref, matrix)| (*uuid_ref, matrix))
|
||||
.collect();
|
||||
|
||||
for (uuid, matrix) in old_modifiers {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modifiers.insert(uuid_ref, matrix);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild structure with fresh references
|
||||
if !self.structure.is_empty() {
|
||||
let old_structure: Vec<(Uuid, Vec<StructureEntry>)> = self
|
||||
.structure
|
||||
.drain()
|
||||
.map(|(uuid_ref, entries)| (*uuid_ref, entries))
|
||||
.collect();
|
||||
|
||||
for (uuid, entries) in old_structure {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.structure.insert(uuid_ref, entries);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Rebuild scale_content with fresh references
|
||||
if !self.scale_content.is_empty() {
|
||||
let old_scale_content: Vec<(Uuid, f32)> = self
|
||||
.scale_content
|
||||
.drain()
|
||||
.map(|(uuid_ref, scale)| (*uuid_ref, scale))
|
||||
.collect();
|
||||
|
||||
for (uuid, scale) in old_scale_content {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.scale_content.insert(uuid_ref, scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Rebuild modified_shape_cache with fresh references
|
||||
if !self.modified_shape_cache.is_empty() {
|
||||
let old_cache: Vec<(Uuid, OnceCell<Shape>)> = self
|
||||
.modified_shape_cache
|
||||
.drain()
|
||||
.map(|(uuid_ref, cell)| (*uuid_ref, cell))
|
||||
.collect();
|
||||
|
||||
for (uuid, cell) in old_cache {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, cell);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No longer needed! Index-based storage means no references to rebuild.
|
||||
// The old rebuild_references() function has been removed entirely.
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.shapes_uuid_to_idx.len()
|
||||
self.uuid_to_idx.len()
|
||||
}
|
||||
|
||||
pub fn has(&self, id: &Uuid) -> bool {
|
||||
self.shapes_uuid_to_idx.contains_key(&id)
|
||||
self.uuid_to_idx.contains_key(id)
|
||||
}
|
||||
|
||||
pub fn get_mut(&mut self, id: &Uuid) -> Option<&mut Shape> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
let idx = *self.uuid_to_idx.get(id)?;
|
||||
Some(&mut self.shapes[idx])
|
||||
}
|
||||
|
||||
pub fn get(&self, id: &Uuid) -> Option<&'a Shape> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
/// Get a shape by UUID. Returns the modified shape if modifiers/structure
|
||||
/// are applied, otherwise returns the base shape.
|
||||
pub fn get(&self, id: &Uuid) -> Option<&Shape> {
|
||||
let idx = *self.uuid_to_idx.get(id)?;
|
||||
|
||||
// SAFETY: We're extending the lifetimes to 'a.
|
||||
// This is safe because:
|
||||
// 1. All internal HashMaps and the shapes Vec have fields with lifetime 'a
|
||||
// 2. The shape at idx won't be moved or reallocated (pre-allocated Vec)
|
||||
// 3. The id is stored in shapes[idx].id which has lifetime 'a
|
||||
// 4. The references won't outlive the ShapesPoolImpl
|
||||
unsafe {
|
||||
let shape_ptr = &self.shapes[idx] as *const Shape;
|
||||
let modifiers_ptr = &self.modifiers as *const HashMap<&'a Uuid, skia::Matrix>;
|
||||
let structure_ptr = &self.structure as *const HashMap<&'a Uuid, Vec<StructureEntry>>;
|
||||
let scale_content_ptr = &self.scale_content as *const HashMap<&'a Uuid, f32>;
|
||||
let cache_ptr = &self.modified_shape_cache as *const HashMap<&'a Uuid, OnceCell<Shape>>;
|
||||
let shape = &self.shapes[idx];
|
||||
|
||||
// Extend the lifetime of id to 'a - safe because it's the same Uuid stored in shapes[idx].id
|
||||
let id_ref: &'a Uuid = &*(id as *const Uuid);
|
||||
// Check if this shape needs modification (has modifiers, structure changes, or is a bool)
|
||||
let needs_modification = shape.is_bool()
|
||||
|| self.modifiers.contains_key(&idx)
|
||||
|| self.structure.contains_key(&idx)
|
||||
|| self.scale_content.contains_key(&idx);
|
||||
|
||||
if (*shape_ptr).is_bool()
|
||||
|| (*modifiers_ptr).contains_key(&id_ref)
|
||||
|| (*structure_ptr).contains_key(&id_ref)
|
||||
|| (*scale_content_ptr).contains_key(&id_ref)
|
||||
{
|
||||
if let Some(cell) = (*cache_ptr).get(&id_ref) {
|
||||
Some(cell.get_or_init(|| {
|
||||
let mut shape = (*shape_ptr).transformed(
|
||||
(*modifiers_ptr).get(&id_ref),
|
||||
(*structure_ptr).get(&id_ref),
|
||||
);
|
||||
if needs_modification {
|
||||
// Check if we have a cached modified version
|
||||
if let Some(cell) = self.modified_shape_cache.get(&idx) {
|
||||
Some(cell.get_or_init(|| {
|
||||
let mut modified_shape =
|
||||
shape.transformed(self.modifiers.get(&idx), self.structure.get(&idx));
|
||||
|
||||
if self.to_update_bool(&shape) {
|
||||
math_bools::update_bool_to_path(&mut shape, self);
|
||||
}
|
||||
if self.to_update_bool(&modified_shape) {
|
||||
math_bools::update_bool_to_path(&mut modified_shape, self);
|
||||
}
|
||||
|
||||
if let Some(scale) = (*scale_content_ptr).get(&id_ref) {
|
||||
shape.scale_content(*scale);
|
||||
}
|
||||
shape
|
||||
}))
|
||||
} else {
|
||||
Some(&*shape_ptr)
|
||||
}
|
||||
if let Some(scale) = self.scale_content.get(&idx) {
|
||||
modified_shape.scale_content(*scale);
|
||||
}
|
||||
modified_shape
|
||||
}))
|
||||
} else {
|
||||
Some(&*shape_ptr)
|
||||
Some(shape)
|
||||
}
|
||||
} else {
|
||||
Some(shape)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,69 +192,68 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
pub fn set_modifiers(&mut self, modifiers: HashMap<Uuid, skia::Matrix>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
let mut modifiers_with_idx = HashMap::with_capacity(modifiers.len());
|
||||
|
||||
let mut modifiers_with_refs = HashMap::with_capacity(modifiers.len());
|
||||
for (uuid, matrix) in modifiers {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
// self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
modifiers_with_refs.insert(uuid_ref, matrix);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
modifiers_with_idx.insert(idx, matrix);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.modifiers = modifiers_with_refs;
|
||||
self.modifiers = modifiers_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_structure(&mut self, structure: HashMap<Uuid, Vec<StructureEntry>>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
let mut structure_with_refs = HashMap::with_capacity(structure.len());
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
let mut structure_with_idx = HashMap::with_capacity(structure.len());
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
|
||||
for (uuid, entries) in structure {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
structure_with_refs.insert(uuid_ref, entries);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
structure_with_idx.insert(idx, entries);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.structure = structure_with_refs;
|
||||
self.structure = structure_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_scale_content(&mut self, scale_content: HashMap<Uuid, f32>) {
|
||||
// Convert HashMap<Uuid, V> to HashMap<&'a Uuid, V> using references from shapes and
|
||||
// Initialize the cache cells because later we don't want to have the mutable pointer
|
||||
let mut scale_content_with_refs = HashMap::with_capacity(scale_content.len());
|
||||
// Convert HashMap<Uuid, V> to HashMap<usize, V> using indices
|
||||
// Initialize the cache cells for affected shapes
|
||||
let mut scale_content_with_idx = HashMap::with_capacity(scale_content.len());
|
||||
let mut ids = Vec::<Uuid>::new();
|
||||
|
||||
for (uuid, value) in scale_content {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
scale_content_with_refs.insert(uuid_ref, value);
|
||||
ids.push(*uuid_ref);
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
scale_content_with_idx.insert(idx, value);
|
||||
ids.push(uuid);
|
||||
}
|
||||
}
|
||||
self.scale_content = scale_content_with_refs;
|
||||
self.scale_content = scale_content_with_idx;
|
||||
|
||||
let all_ids = shapes::all_with_ancestors(&ids, self, true);
|
||||
for uuid in all_ids {
|
||||
if let Some(uuid_ref) = self.get_uuid_ref(&uuid) {
|
||||
self.modified_shape_cache.insert(uuid_ref, OnceCell::new());
|
||||
if let Some(idx) = self.uuid_to_idx.get(&uuid).copied() {
|
||||
self.modified_shape_cache.insert(idx, OnceCell::new());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -349,47 +265,33 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
self.scale_content = HashMap::default();
|
||||
}
|
||||
|
||||
/// Get a reference to the Uuid stored in a shape, if it exists
|
||||
pub fn get_uuid_ref(&self, id: &Uuid) -> Option<&'a Uuid> {
|
||||
let idx = *self.shapes_uuid_to_idx.get(&id)?;
|
||||
// SAFETY: We're returning a reference with lifetime 'a to a Uuid stored
|
||||
// in the shapes Vec. This is safe because the Vec is stable (pre-allocated)
|
||||
// and won't be reallocated.
|
||||
unsafe { Some(&*(&self.shapes[idx].id as *const Uuid)) }
|
||||
}
|
||||
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl<'a> {
|
||||
pub fn subtree(&self, id: &Uuid) -> ShapesPoolImpl {
|
||||
let Some(shape) = self.get(id) else {
|
||||
panic!("Subtree not found");
|
||||
};
|
||||
|
||||
let mut shapes = vec![];
|
||||
let mut idx = 0;
|
||||
let mut shapes_uuid_to_idx = HashMap::default();
|
||||
let mut new_idx = 0;
|
||||
let mut uuid_to_idx = HashMap::default();
|
||||
|
||||
for id in shape.all_children_iter(self, true, true) {
|
||||
let Some(shape) = self.get(&id) else {
|
||||
for child_id in shape.all_children_iter(self, true, true) {
|
||||
let Some(child_shape) = self.get(&child_id) else {
|
||||
panic!("Not found");
|
||||
};
|
||||
shapes.push(shape.clone());
|
||||
|
||||
let id_ref: &'a Uuid = unsafe { &*(&self.shapes[idx].id as *const Uuid) };
|
||||
shapes_uuid_to_idx.insert(id_ref, idx);
|
||||
idx += 1;
|
||||
shapes.push(child_shape.clone());
|
||||
uuid_to_idx.insert(child_id, new_idx);
|
||||
new_idx += 1;
|
||||
}
|
||||
|
||||
let mut result = ShapesPoolImpl {
|
||||
ShapesPoolImpl {
|
||||
shapes,
|
||||
counter: idx,
|
||||
shapes_uuid_to_idx,
|
||||
counter: new_idx,
|
||||
uuid_to_idx,
|
||||
modified_shape_cache: HashMap::default(),
|
||||
modifiers: HashMap::default(),
|
||||
structure: HashMap::default(),
|
||||
scale_content: HashMap::default(),
|
||||
};
|
||||
result.rebuild_references();
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
fn to_update_bool(&self, shape: &Shape) -> bool {
|
||||
@@ -398,11 +300,21 @@ impl<'a> ShapesPoolImpl<'a> {
|
||||
}
|
||||
|
||||
let default = &Matrix::default();
|
||||
let parent_modifier = self.modifiers.get(&shape.id).unwrap_or(default);
|
||||
|
||||
// Get parent modifier by index
|
||||
let parent_idx = self.uuid_to_idx.get(&shape.id);
|
||||
let parent_modifier = parent_idx
|
||||
.and_then(|idx| self.modifiers.get(idx))
|
||||
.unwrap_or(default);
|
||||
|
||||
// Returns true if the transform of any child is different to the parent's
|
||||
shape.all_children_iter(self, true, false).any(|id| {
|
||||
!math::is_close_matrix(parent_modifier, self.modifiers.get(&id).unwrap_or(default))
|
||||
shape.all_children_iter(self, true, false).any(|child_id| {
|
||||
let child_modifier = self
|
||||
.uuid_to_idx
|
||||
.get(&child_id)
|
||||
.and_then(|idx| self.modifiers.get(idx))
|
||||
.unwrap_or(default);
|
||||
!math::is_close_matrix(parent_modifier, child_modifier)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{with_current_shape_mut, STATE};
|
||||
use macros::ToJs;
|
||||
|
||||
mod align;
|
||||
mod constraints;
|
||||
pub mod constraints;
|
||||
mod flex;
|
||||
mod grid;
|
||||
|
||||
|
||||
173
render-wasm/src/wasm/shapes/base_props.rs
Normal file
173
render-wasm/src/wasm/shapes/base_props.rs
Normal file
@@ -0,0 +1,173 @@
|
||||
use crate::mem;
|
||||
use crate::shapes::{BlendMode, ConstraintH, ConstraintV};
|
||||
use crate::utils::uuid_from_u32_quartet;
|
||||
use crate::uuid::Uuid;
|
||||
use crate::wasm::blend::RawBlendMode;
|
||||
use crate::wasm::layouts::constraints::{RawConstraintH, RawConstraintV};
|
||||
use crate::{with_state_mut, STATE};
|
||||
|
||||
use super::RawShapeType;
|
||||
|
||||
/// Binary layout for batched shape base properties:
|
||||
///
|
||||
/// | Offset | Size | Field | Type |
|
||||
/// |--------|------|--------------|-----------------------------------|
|
||||
/// | 0 | 16 | id | UUID (4 × u32 LE) |
|
||||
/// | 16 | 16 | parent_id | UUID (4 × u32 LE) |
|
||||
/// | 32 | 1 | shape_type | u8 |
|
||||
/// | 33 | 1 | flags | u8 (bit0: clip, bit1: hidden) |
|
||||
/// | 34 | 1 | blend_mode | u8 |
|
||||
/// | 35 | 1 | constraint_h | u8 (0xFF = None) |
|
||||
/// | 36 | 1 | constraint_v | u8 (0xFF = None) |
|
||||
/// | 37 | 3 | padding | - |
|
||||
/// | 40 | 4 | opacity | f32 LE |
|
||||
/// | 44 | 4 | rotation | f32 LE |
|
||||
/// | 48 | 24 | transform | 6 × f32 LE (a,b,c,d,e,f) |
|
||||
/// | 72 | 16 | selrect | 4 × f32 LE (x1,y1,x2,y2) |
|
||||
/// | 88 | 16 | corners | 4 × f32 LE (r1,r2,r3,r4) |
|
||||
/// |--------|------|--------------|-----------------------------------|
|
||||
/// | Total | 104 | | |
|
||||
pub const BASE_PROPS_SIZE: usize = 104;
|
||||
|
||||
const FLAG_CLIP_CONTENT: u8 = 0b0000_0001;
|
||||
const FLAG_HIDDEN: u8 = 0b0000_0010;
|
||||
const CONSTRAINT_NONE: u8 = 0xFF;
|
||||
|
||||
/// Reads a f32 from a byte slice at the given offset (little-endian)
|
||||
#[inline]
|
||||
fn read_f32_le(bytes: &[u8], offset: usize) -> f32 {
|
||||
f32::from_le_bytes([
|
||||
bytes[offset],
|
||||
bytes[offset + 1],
|
||||
bytes[offset + 2],
|
||||
bytes[offset + 3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Reads a u32 from a byte slice at the given offset (little-endian)
|
||||
#[inline]
|
||||
fn read_u32_le(bytes: &[u8], offset: usize) -> u32 {
|
||||
u32::from_le_bytes([
|
||||
bytes[offset],
|
||||
bytes[offset + 1],
|
||||
bytes[offset + 2],
|
||||
bytes[offset + 3],
|
||||
])
|
||||
}
|
||||
|
||||
/// Parses UUID from bytes at given offset
|
||||
#[inline]
|
||||
fn read_uuid(bytes: &[u8], offset: usize) -> Uuid {
|
||||
uuid_from_u32_quartet(
|
||||
read_u32_le(bytes, offset),
|
||||
read_u32_le(bytes, offset + 4),
|
||||
read_u32_le(bytes, offset + 8),
|
||||
read_u32_le(bytes, offset + 12),
|
||||
)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub extern "C" fn set_shape_base_props() {
|
||||
let bytes = mem::bytes();
|
||||
|
||||
if bytes.len() < BASE_PROPS_SIZE {
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse all fields from the buffer
|
||||
let id = read_uuid(&bytes, 0);
|
||||
let parent_id = read_uuid(&bytes, 16);
|
||||
let shape_type = bytes[32];
|
||||
let flags = bytes[33];
|
||||
let blend_mode = bytes[34];
|
||||
let constraint_h = bytes[35];
|
||||
let constraint_v = bytes[36];
|
||||
// bytes[37..40] are padding
|
||||
|
||||
let opacity = read_f32_le(&bytes, 40);
|
||||
let rotation = read_f32_le(&bytes, 44);
|
||||
|
||||
// Transform matrix (a, b, c, d, e, f)
|
||||
let transform_a = read_f32_le(&bytes, 48);
|
||||
let transform_b = read_f32_le(&bytes, 52);
|
||||
let transform_c = read_f32_le(&bytes, 56);
|
||||
let transform_d = read_f32_le(&bytes, 60);
|
||||
let transform_e = read_f32_le(&bytes, 64);
|
||||
let transform_f = read_f32_le(&bytes, 68);
|
||||
|
||||
// Selrect (x1, y1, x2, y2)
|
||||
let selrect_x1 = read_f32_le(&bytes, 72);
|
||||
let selrect_y1 = read_f32_le(&bytes, 76);
|
||||
let selrect_x2 = read_f32_le(&bytes, 80);
|
||||
let selrect_y2 = read_f32_le(&bytes, 84);
|
||||
|
||||
// Corners (r1, r2, r3, r4)
|
||||
let corner_r1 = read_f32_le(&bytes, 88);
|
||||
let corner_r2 = read_f32_le(&bytes, 92);
|
||||
let corner_r3 = read_f32_le(&bytes, 96);
|
||||
let corner_r4 = read_f32_le(&bytes, 100);
|
||||
|
||||
// Decode flags
|
||||
let clip_content = (flags & FLAG_CLIP_CONTENT) != 0;
|
||||
let hidden = (flags & FLAG_HIDDEN) != 0;
|
||||
|
||||
// Convert raw enum values
|
||||
let shape_type_enum = RawShapeType::from(shape_type);
|
||||
let blend_mode_enum: BlendMode = RawBlendMode::from(blend_mode).into();
|
||||
|
||||
let constraint_h_opt: Option<ConstraintH> = if constraint_h == CONSTRAINT_NONE {
|
||||
None
|
||||
} else {
|
||||
Some(RawConstraintH::from(constraint_h).into())
|
||||
};
|
||||
|
||||
let constraint_v_opt: Option<ConstraintV> = if constraint_v == CONSTRAINT_NONE {
|
||||
None
|
||||
} else {
|
||||
Some(RawConstraintV::from(constraint_v).into())
|
||||
};
|
||||
|
||||
with_state_mut!(state, {
|
||||
// Select/create the shape
|
||||
state.use_shape(id);
|
||||
|
||||
// Set parent relationship
|
||||
state.set_parent_for_current_shape(parent_id);
|
||||
|
||||
// Mark shape as touched
|
||||
state.touch_current();
|
||||
|
||||
// Apply all properties to the current shape
|
||||
if let Some(shape) = state.current_shape_mut() {
|
||||
// Type
|
||||
shape.set_shape_type(shape_type_enum.into());
|
||||
|
||||
// Boolean flags
|
||||
shape.set_clip(clip_content);
|
||||
shape.set_hidden(hidden);
|
||||
|
||||
// Blend mode and opacity
|
||||
shape.set_blend_mode(blend_mode_enum);
|
||||
shape.set_opacity(opacity);
|
||||
|
||||
// Constraints
|
||||
shape.set_constraint_h(constraint_h_opt);
|
||||
shape.set_constraint_v(constraint_v_opt);
|
||||
|
||||
// Transform
|
||||
shape.set_rotation(rotation);
|
||||
shape.set_transform(
|
||||
transform_a,
|
||||
transform_b,
|
||||
transform_c,
|
||||
transform_d,
|
||||
transform_e,
|
||||
transform_f,
|
||||
);
|
||||
|
||||
// Geometry
|
||||
shape.set_selrect(selrect_x1, selrect_y1, selrect_x2, selrect_y2);
|
||||
shape.set_corners((corner_r1, corner_r2, corner_r3, corner_r4));
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod base_props;
|
||||
|
||||
use macros::ToJs;
|
||||
|
||||
use crate::shapes::{Bool, Frame, Group, Path, Rect, SVGRaw, TextContent, Type};
|
||||
Reference in New Issue
Block a user