Compare commits

...

44 Commits

Author SHA1 Message Date
Andrey Antukh
4397ede5c1 Merge branch 'staging-render' into develop 2026-01-21 10:18:15 +01:00
Andrey Antukh
ff25df0457 Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-21 10:17:22 +01:00
Eva Marco
8c7fd0af4b 🐛 Fix shadow reference validation (#8132) 2026-01-21 09:17:03 +01:00
Andrey Antukh
cf46051f56 🔥 Remove .traivis.yml file from the repository 2026-01-20 19:40:31 +01:00
Luis de Dios
079b3fbfad ♻️ Extract and create panel title component (#8090) 2026-01-20 18:56:25 +01:00
Andrey Antukh
299f628951 Merge pull request #8123 from penpot/GlobalStar117-fix/token-validation-crash
🐛 Fix Penpot crash when setting some name in Design tokens
2026-01-20 18:53:05 +01:00
David Barragán Merino
32d0fe6463 🔧 Use selfhosted runner 01 to generate the bundle 2026-01-20 18:09:18 +01:00
Andrey Antukh
6393330ee1 Merge remote-tracking branch 'origin/staging' 2026-01-20 16:25:10 +01:00
Andrey Antukh
8252bc485e 📚 Fix oidc callback related documentation issue 2026-01-20 16:24:12 +01:00
Andrey Antukh
cecd3d4a90 📎 Update changelog 2026-01-20 16:00:57 +01:00
Eva Marco
1c2c0987f5 🐛 Fix schema validation for references from other sets 2026-01-20 15:51:43 +01:00
Globalstar117
0418147e74 🐛 Add error handler to token form validation to prevent crash
When creating a token with a name that conflicts with existing
hierarchical token names (e.g., 'accent-color' when 'accent-color.blue.dark'
exists), the validation throws an error via rx/throw. However, the
rx/subs! subscriber in generic_form.cljs had no error handler, causing
an unhandled exception that resulted in an 'Internal Error' crash.

This fix adds an error handler that:
1. Catches validation errors from the reactive stream
2. Uses humanize-errors to convert them to user-friendly messages
3. Displays the error in the form's extra-errors field

Before: Crash with 'Internal Error' dialog
After: Form shows validation error message

Fixes #8110

---
This is a Gittensor contribution.
gittensor:user:GlobalStar117
2026-01-20 15:51:25 +01:00
Alonso Torres
47775a9e2c Merge pull request #8134 from penpot/alotor-fix-plugins-export
🐛 Fix problem with export in plugins
2026-01-20 15:03:04 +01:00
Eva Marco
7499a5bca6 ♻️ Replace opacity input (#8072)
*  Replace opacity numeric input

*  Add test
2026-01-20 14:30:35 +01:00
Xaviju
6cd5bc76d7 💄 Replace current themes switch with DS switch (#8131) 2026-01-20 14:17:25 +01:00
Andrey Antukh
8191d04114 Use non-legacy config example on docker compose file 2026-01-20 13:25:55 +01:00
David Barragán Merino
bbe6ee2e19 🔧 Define a different temporary config file for each execution 2026-01-20 12:59:56 +01:00
David Barragán Merino
fb6d8309b6 🔧 Prevent error 429 downloading docker images from dockerhub 2026-01-20 12:59:56 +01:00
Alejandro Alonso
b7c2d9a079 Merge pull request #8130 from penpot/superalex-improve-zoom-pan-performance-7
🐛 Fix some tiles disappear after fast zoom and pan
2026-01-20 12:56:02 +01:00
Alejandro Alonso
aeb34a6f64 Merge pull request #8109 from penpot/superalex-fix-text-selrect-calculation
🐛 Render wasm typography token issues
2026-01-20 12:54:45 +01:00
Alejandro Alonso
6fa0c3af0c 🐛 Fix some tiles disappear after fast zoom and pan 2026-01-20 12:40:01 +01:00
Alejandro Alonso
260b9fb040 🐛 Fix texts with auto size updated via tokens with render wasm
activated
2026-01-20 12:39:17 +01:00
Alejandro Alonso
884954f4ff 🐛 Fix text selrect calculation 2026-01-20 12:37:57 +01:00
Andrey Antukh
88f0f75174 Merge pull request #8129 from penpot/niwinz-staging-bugfix-1
 Several improvements for build process
2026-01-20 12:26:55 +01:00
Andrey Antukh
1ffa956251 Include timestamp on version tag 2026-01-20 12:26:39 +01:00
Andrey Antukh
31054099ff Use pseudo-names on release builds of frontend (#8105) 2026-01-20 12:26:39 +01:00
Andrey Antukh
689467bcf9 📎 Update changelog 2026-01-20 12:25:43 +01:00
Andrey Antukh
7724450037 Merge remote-tracking branch 'origin/staging-render' into develop 2026-01-20 12:18:04 +01:00
Xaviju
368fa954ce Remove tokens tree node (#8042) 2026-01-20 11:00:13 +01:00
Eva Marco
983487d73c 🐛 Fix shadow token reference validation (#8128) 2026-01-20 10:56:27 +01:00
Andrey Antukh
6fd0f5377c Merge remote-tracking branch 'origin/staging' into staging-render 2026-01-20 10:08:58 +01:00
Elena Torró
eb54bc485e Merge pull request #8120 from penpot/alotor-fix-flex-layout
🐛 Fix problems with layout
2026-01-20 10:00:24 +01:00
Elena Torró
12c24a36b4 Merge pull request #8122 from penpot/fix-thumbnail-generation
🐛 Fix problem with thumbnail generation
2026-01-20 09:59:34 +01:00
Alejandro Alonso
324d54ad28 🐛 Fix set all rounded corners to 0 2026-01-20 09:34:06 +01:00
alonso.torres
f42ff27f3d 🐛 Fix problem with bools 2026-01-19 17:05:04 +01:00
alonso.torres
2c1cc89f53 🐛 Fix problem with thumbnail generation 2026-01-19 12:54:15 +01:00
alonso.torres
498b0b30fe 🐛 Fix problems with layout 2026-01-19 12:17:58 +01:00
Elena Torró
89f40dcda2 🔧 Move WebGL context error message to 'errors' namespace (#8117) 2026-01-19 11:24:19 +01:00
Elena Torró
ccac7bd510 Merge pull request #8108 from penpot/ladybenko-13022-blur-page
🎉 Apply blur effect when switching pages
2026-01-19 11:04:31 +01:00
Belén Albeza
43d1d127dc 🎉 Apply blur effect to previous canvas pixels while setting wasm objects 2026-01-16 13:04:59 +01:00
Belén Albeza
8bd3ef717c 🎉 Apply blur to canvas when switching pages 2026-01-16 13:04:59 +01:00
Andrey Antukh
5c71c57dd9 Merge tag '2.12.1' 2025-12-30 15:37:30 +01:00
Andrey Antukh
5abc1aafb4 Merge tag '2.12.0-RC3' 2025-12-12 12:19:29 +01:00
Andrey Antukh
935728aa39 🔧 Backport build-tag github workflow from develop 2025-12-05 10:26:01 +01:00
79 changed files with 1360 additions and 682 deletions

View File

@@ -40,7 +40,7 @@ on:
jobs: jobs:
build-bundle: build-bundle:
name: Build and Upload Penpot Bundle name: Build and Upload Penpot Bundle
runs-on: ubuntu-24.04 runs-on: penpot-runner-01
env: env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

View File

@@ -10,6 +10,11 @@ jobs:
runs-on: penpot-runner-02 runs-on: penpot-runner-02
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

View File

@@ -22,6 +22,11 @@ jobs:
runs-on: penpot-runner-02 runs-on: penpot-runner-02
steps: steps:
- name: Set common environment variables
run: |
# Each job execution will use its own docker configuration.
echo "DOCKER_CONFIG=${{ runner.temp }}/.docker-${{ github.run_id }}-${{ github.job }}" >> $GITHUB_ENV
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
@@ -66,6 +71,15 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
# To avoid the “429 Too Many Requests” error when downloading
# images from DockerHub for unregistered users.
# https://docs.docker.com/docker-hub/usage/
- name: Login to DockerHub Registry
uses: docker/login-action@v3
with:
username: ${{ secrets.PUB_DOCKER_USERNAME }}
password: ${{ secrets.PUB_DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) - name: Extract metadata (tags, labels)
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5

View File

@@ -1,40 +0,0 @@
dist: xenial
language: generic
sudo: required
cache:
directories:
- $HOME/.m2
services:
- docker
branches:
only:
- master
- develop
install:
- curl -O https://download.clojure.org/install/linux-install-1.10.1.447.sh
- chmod +x linux-install-1.10.1.447.sh
- sudo ./linux-install-1.10.1.447.sh
before_script:
- env | sort
script:
- ./manage.sh build-devenv
- ./manage.sh run-frontend-tests
- ./manage.sh run-backend-tests
- ./manage.sh build-images
- ./manage.sh run
after_script:
- docker images
notifications:
email: false
env:
- NODE_VERSION=10.16.0

View File

@@ -14,7 +14,8 @@
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966) - Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916) - Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017) - Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
- Change the default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051) - Remove tokens path node and bulk remove tokens [Taiga #13007](https://tree.taiga.io/project/penpot/us/13007)
- Replace themes management modal radio buttons for switches [Taiga #9215](https://tree.taiga.io/project/penpot/us/9215)
### :bug: Bugs fixed ### :bug: Bugs fixed
@@ -22,6 +23,8 @@
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040) - 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 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 error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- 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)
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)
@@ -169,7 +172,6 @@ example. It's still usable as before, we just removed the example.
- Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be - Deprecated configuration variables with the prefix `PENPOT_ASSETS_*`, and will be
removed in future versions: removed in future versions:
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its - The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`) values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY` - The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`

View File

@@ -487,6 +487,7 @@
:vertical-margin #{:spacing :dimensions} :vertical-margin #{:spacing :dimensions}
:sided-margins #{:spacing :dimensions} :sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number} :line-height #{:line-height :number}
:opacity #{:opacity}
:font-size #{:font-size} :font-size #{:font-size}
:letter-spacing #{:letter-spacing} :letter-spacing #{:letter-spacing}
:fill #{:color} :fill #{:color}

View File

@@ -152,9 +152,9 @@ services:
# AWS_ACCESS_KEY_ID: <KEY_ID> # AWS_ACCESS_KEY_ID: <KEY_ID>
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY> # AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3 # PENPOT_OBJECTS_STORAGE_BACKEND: s3
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <ENDPOINT> # PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME> # PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>
## Telemetry. When enabled, a periodical process will send anonymous data about this ## Telemetry. When enabled, a periodical process will send anonymous data about this
## instance. Telemetry data will enable us to learn how the application is used, ## instance. Telemetry data will enable us to learn how the application is used,

View File

@@ -114,14 +114,7 @@ configuration.
The callback has the following format: The callback has the following format:
```html ```html
https://<your_domain>/api/auth/oauth/<oauth_provider>/callback https://<your_domain>/api/auth/oidc/callback
```
You will need to change <your_domain> and <oauth_provider> according to your setup.
This is how it looks with Gitlab provider:
```html
https://<your_domain>/api/auth/oauth/gitlab/callback
``` ```
#### Google #### Google

View File

@@ -101,6 +101,70 @@ test.describe("Tokens: Apply token", () => {
await expect(brTokenPillXL).not.toBeVisible(); await expect(brTokenPillXL).not.toBeVisible();
}); });
test("User applies opacity token to a shape from sidebar", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
// Open tokens sections on left sidebar
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
// Unfold opacity tokens
await page.getByRole("button", { name: "Opacity 3" }).click();
await expect(
tokensSidebar.getByRole("button", { name: "opacity", exact: true }),
).toBeVisible();
await tokensSidebar
.getByRole("button", { name: "opacity", exact: true })
.click();
await expect(
tokensSidebar.getByRole("button", { name: "opacity.high" }),
).toBeVisible();
// Apply opacity token from token panels
await tokensSidebar.getByRole("button", { name: "opacity.high" }).click();
// Check if opacity sections is visible on right sidebar
const layerMenuSection = page.getByRole("region", {
name: "layer-menu-section",
});
await expect(layerMenuSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const opacityHighPill = layerMenuSection.getByRole("button", {
name: "opacity.high",
});
await expect(opacityHighPill).toBeVisible();
// Detach token from design tab on right sidebar
const detachButton = layerMenuSection.getByRole("button", {
name: "Detach token",
});
await detachButton.click();
// Open dropdown from input
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' });
await expect(opacityLowOption).toBeVisible();
await opacityLowOption.click();
await expect(opacityHighPill).not.toBeVisible();
const opacityLowPill = layerMenuSection.getByRole("button", {
name: "opacity.low",
});
await expect(opacityLowPill).toBeVisible();
});
test("User applies typography token to a text shape", async ({ page }) => { test("User applies typography token to a text shape", async ({ page }) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } = const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTypographyTokensFile(page); await setupTypographyTokensFile(page);
@@ -129,189 +193,6 @@ test.describe("Tokens: Apply token", () => {
await expect(fontSizeInput).toHaveValue("100"); await expect(fontSizeInput).toHaveValue("100");
}); });
test("User edits typography token and all fields are valid", async ({
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
await tokensSidebar
.getByRole("button")
.filter({ hasText: "Typography" })
.click();
// Open edit modal for "Full" typography token
const token = tokensSidebar.getByRole("button", { name: "Full" });
await token.click({ button: "right" });
await page.getByText("Edit token").click();
// Modal opens
await expect(tokensUpdateCreateModal).toBeVisible();
const saveButton = tokensUpdateCreateModal.getByRole("button", {
name: /save/i,
});
// Fill font-family to verify to verify that input value doesn't get split into list of characters
const fontFamilyField = tokensUpdateCreateModal
.getByLabel("Font family")
.first();
await fontFamilyField.fill("OneWord");
// Invalidate incorrect values for font size
const fontSizeField = tokensUpdateCreateModal.getByLabel(/Font Size/i);
await fontSizeField.fill("invalid");
await expect(
tokensUpdateCreateModal.getByText(/Invalid token value:/),
).toBeVisible();
await expect(saveButton).toBeDisabled();
// Show error with line-height depending on invalid font-size
await fontSizeField.fill("");
await expect(saveButton).toBeDisabled();
// Fill in values for all fields and verify they persist when switching tabs
await fontSizeField.fill("16");
await expect(saveButton).toBeEnabled();
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
const letterSpacingField =
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i);
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
const textDecorationField =
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
// Capture all values before switching tabs
const originalValues = {
fontSize: await fontSizeField.inputValue(),
fontFamily: await fontFamilyField.inputValue(),
fontWeight: await fontWeightField.inputValue(),
letterSpacing: await letterSpacingField.inputValue(),
lineHeight: await lineHeightField.inputValue(),
textCase: await textCaseField.inputValue(),
textDecoration: await textDecorationField.inputValue(),
};
// Switch to reference tab and back to composite tab
const referenceTabButton =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
// Empty reference tab should be disabled
await expect(saveButton).toBeDisabled();
const compositeTabButton =
tokensUpdateCreateModal.getByTestId("composite-opt");
await compositeTabButton.click();
// Filled composite tab should be enabled
await expect(saveButton).toBeEnabled();
// Verify all values are preserved after switching tabs
await expect(fontSizeField).toHaveValue(originalValues.fontSize);
await expect(fontFamilyField).toHaveValue(originalValues.fontFamily);
await expect(fontWeightField).toHaveValue(originalValues.fontWeight);
await expect(letterSpacingField).toHaveValue(originalValues.letterSpacing);
await expect(lineHeightField).toHaveValue(originalValues.lineHeight);
await expect(textCaseField).toHaveValue(originalValues.textCase);
await expect(textDecorationField).toHaveValue(
originalValues.textDecoration,
);
await saveButton.click();
// Modal should close, token should be visible (with new name) in sidebar
await expect(tokensUpdateCreateModal).not.toBeVisible();
});
test("User cant submit empty typography token or reference", async ({
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("typography.empty");
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
// Insert a value and then delete it
await valueField.fill("1");
await valueField.fill("");
// Submit button should be disabled when field is empty
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeDisabled();
// Switch to reference tab, should not be submittable either
const referenceTabButton =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
await expect(submitButton).toBeDisabled();
});
test("User adds typography token with reference", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const newTokenTitle = "NewReference";
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newTokenTitle);
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Use a reference",
});
referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{Full}");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
const resolvedValue =
await tokensUpdateCreateModal.getByText("Resolved value:");
await expect(resolvedValue).toBeVisible();
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
await expect(resolvedValue).toContainText("Font Size: 100");
await expect(resolvedValue).toContainText("Font Weight: 300");
await expect(resolvedValue).toContainText("Letter Spacing: 2");
await expect(resolvedValue).toContainText("Text Case: uppercase");
await expect(resolvedValue).toContainText("Text Decoration: underline");
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
const newToken = tokensSidebar.getByRole("button", {
name: newTokenTitle,
});
await expect(newToken).toBeVisible();
});
test("User adds shadow token with multiple shadows and applies it to shape", async ({ test("User adds shadow token with multiple shadows and applies it to shape", async ({
page, page,
}) => { }) => {

View File

@@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => {
await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json"); await BaseWebSocketPage.mockRPC(page, "get-teams", "get-teams-tokens.json");
}); });
test.describe("Tokens - CRUD", () => { test.describe("Tokens - creation", () => {
test("User creates border radius token", async ({ page }) => { test("User creates border radius token", async ({ page }) => {
await testTokenCreationFlow(page, { await testTokenCreationFlow(page, {
tokenLabel: "Border Radius", tokenLabel: "Border Radius",
@@ -1008,6 +1008,41 @@ test.describe("Tokens - CRUD", () => {
).toBeEnabled(); ).toBeEnabled();
}); });
test("User cant submit empty typography token or reference", async ({
page,
}) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill("typography.empty");
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
// Insert a value and then delete it
await valueField.fill("1");
await valueField.fill("");
// Submit button should be disabled when field is empty
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeDisabled();
// Switch to reference tab, should not be submittable either
const referenceTabButton =
tokensUpdateCreateModal.getByTestId("reference-opt");
await referenceTabButton.click();
await expect(submitButton).toBeDisabled();
});
test("User creates typography token", async ({ page }) => { test("User creates typography token", async ({ page }) => {
const emptyNameError = "Name should be at least 1 character"; const emptyNameError = "Name should be at least 1 character";
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } = const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
@@ -1256,6 +1291,228 @@ test.describe("Tokens - CRUD", () => {
).toBeEnabled(); ).toBeEnabled();
}); });
test("User adds typography token with reference", async ({ page }) => {
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
await setupTypographyTokensFile(page);
const newTokenTitle = "NewReference";
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Typography" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
await nameField.fill(newTokenTitle);
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
name: "Use a reference",
});
referenceTabButton.click();
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
name: "Reference",
});
await referenceField.fill("{Full}");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
const resolvedValue =
await tokensUpdateCreateModal.getByText("Resolved value:");
await expect(resolvedValue).toBeVisible();
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
await expect(resolvedValue).toContainText("Font Size: 100");
await expect(resolvedValue).toContainText("Font Weight: 300");
await expect(resolvedValue).toContainText("Letter Spacing: 2");
await expect(resolvedValue).toContainText("Text Case: uppercase");
await expect(resolvedValue).toContainText("Text Decoration: underline");
await expect(submitButton).toBeEnabled();
await submitButton.click();
await expect(tokensUpdateCreateModal).not.toBeVisible();
const newToken = tokensSidebar.getByRole("button", {
name: newTokenTitle,
});
await expect(newToken).toBeVisible();
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
const colorToken = tokensSidebar.getByRole("button", {
name: "100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
});
test("User creates grouped color token", async ({ page }) => {
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User duplicate color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
const colorToken = tokensSidebar.getByRole("button", {
name: "100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
test.describe("Tokens tab - edition", () => {
test("User edits typography token and all fields are valid", async ({ test("User edits typography token and all fields are valid", async ({
page, page,
}) => { }) => {
@@ -1388,67 +1645,7 @@ test.describe("Tokens - CRUD", () => {
await expect(colorTokenChanged).toBeVisible(); await expect(colorTokenChanged).toBeVisible();
}); });
test("User creates grouped color token", async ({ page }) => { test("User edits color token color while keeping custom color space", async ({
const { workspacePage, tokensUpdateCreateModal, tokensSidebar } =
await setupEmptyTokensFile(page);
await tokensSidebar
.getByRole("button", { name: "Add Token: Color" })
.click();
// Create grouped color token with mouse
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const valueField = tokensUpdateCreateModal.getByLabel("Value");
await nameField.click();
await nameField.fill("dark.primary");
await valueField.click();
await valueField.fill("red");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
await expect(submitButton).toBeEnabled();
await submitButton.click();
await unfoldTokenTree(tokensSidebar, "color", "dark.primary");
await expect(tokensSidebar.getByLabel("primary")).toBeEnabled();
});
test("User cant create regular token with value missing", async ({
page,
}) => {
const { tokensUpdateCreateModal } = await setupEmptyTokensFile(page);
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
await tokensTabPanel
.getByRole("button", { name: "Add Token: Color" })
.click();
await expect(tokensUpdateCreateModal).toBeVisible();
const nameField = tokensUpdateCreateModal.getByLabel("Name");
const submitButton = tokensUpdateCreateModal.getByRole("button", {
name: "Save",
});
// Initially submit button should be disabled
await expect(submitButton).toBeDisabled();
// Fill in name but leave value empty
await nameField.click();
await nameField.fill("primary");
// Submit button should remain disabled when value is empty
await expect(submitButton).toBeDisabled();
});
test("User changes color token color while keeping custom color space", async ({
page, page,
}) => { }) => {
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } = const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
@@ -1502,30 +1699,9 @@ test.describe("Tokens - CRUD", () => {
await valueSaturationSelector.click({ position: { x: 0, y: 0 } }); await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
await expect(valueField).toHaveValue(/^rgba(.*)$/); await expect(valueField).toHaveValue(/^rgba(.*)$/);
}); });
});
test("User duplicate color token", async ({ page }) => { test.describe("Tokens tab - delete", () => {
const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
const colorToken = tokensSidebar.getByRole("button", {
name: "100",
});
await colorToken.click({ button: "right" });
await expect(tokenContextMenuForToken).toBeVisible();
await tokenContextMenuForToken.getByText("Duplicate token").click();
await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(
tokensSidebar.getByRole("button", { name: "colors.blue.100-copy" }),
).toBeVisible();
});
test("User delete color token", async ({ page }) => { test("User delete color token", async ({ page }) => {
const { tokensSidebar, tokenContextMenuForToken } = const { tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page); await setupTokensFile(page);
@@ -1546,4 +1722,40 @@ test.describe("Tokens - CRUD", () => {
await expect(tokenContextMenuForToken).not.toBeVisible(); await expect(tokenContextMenuForToken).not.toBeVisible();
await expect(colorToken).not.toBeVisible(); await expect(colorToken).not.toBeVisible();
}); });
test("User removes node and all child tokens", async ({ page }) => {
const { tokensSidebar, workspacePage } = await setupTokensFile(page);
await expect(tokensSidebar).toBeVisible();
// Expand color tokens
unfoldTokenTree(tokensSidebar, "color", "colors.blue.100");
// Verify that the node and child token are visible before deletion
const colorNode = tokensSidebar.getByRole("button", {
name: "blue",
exact: true,
});
const colorNodeToken = tokensSidebar.getByRole("button", {
name: "100",
});
// Select a node and right click on it to open context menu
await expect(colorNode).toBeVisible();
await expect(colorNodeToken).toBeVisible();
await colorNode.click({ button: "right" });
// select "Delete" from the context menu
const deleteNodeButton = page.getByRole("button", {
name: "Delete",
exact: true,
});
await expect(deleteNodeButton).toBeVisible();
await deleteNodeButton.click();
// Verify that the node is removed
await expect(colorNode).not.toBeVisible();
// Verify that child token is also removed
await expect(colorNodeToken).not.toBeVisible();
});
}); });

View File

@@ -17,7 +17,6 @@
--app-background: var(--color-background-primary); --app-background: var(--color-background-primary);
--loader-background: var(--color-background-primary); --loader-background: var(--color-background-primary);
--panel-title-background-color: var(--color-background-secondary);
// BUTTONS // BUTTONS
--button-foreground-hover: var(--color-accent-primary); --button-foreground-hover: var(--color-accent-primary);

View File

@@ -17,17 +17,18 @@
<meta name="twitter:site" content="@penpotapp"> <meta name="twitter:site" content="@penpotapp">
<meta name="twitter:creator" content="@penpotapp"> <meta name="twitter:creator" content="@penpotapp">
<meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)"> <meta name="theme-color" content="#FFFFFF" media="(prefers-color-scheme: light)">
<link id="theme" href="css/main.css?version={{& version}}" rel="stylesheet" type="text/css" /> <link id="theme" href="css/main.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{#isDebug}} {{#isDebug}}
<link href="css/debug.css?version={{& version}}" rel="stylesheet" type="text/css" /> <link href="css/debug.css?version={{& version_tag}}" rel="stylesheet" type="text/css" />
{{/isDebug}} {{/isDebug}}
<link rel="icon" href="images/favicon.png" /> <link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
<script type="importmap">{{& manifest.importmap }}</script> <script type="importmap">{{& manifest.importmap }}</script>
<script type="module"> <script type="module">
globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersion = "{{& version}}";
globalThis.penpotVersionTag = "{{& version_tag}}";
globalThis.penpotBuildDate = "{{& build_date}}"; globalThis.penpotBuildDate = "{{& build_date}}";
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
</script> </script>

View File

@@ -3,10 +3,11 @@
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Penpot - Rasterizer</title> <title>Penpot - Rasterizer</title>
<link rel="icon" href="images/favicon.png" /> <link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
<script> <script>
globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersion = "{{& version}}";
globalThis.penpotVersionTag = "{{& version_tag}}";
globalThis.penpotBuildDate = "{{& build_date}}"; globalThis.penpotBuildDate = "{{& build_date}}";
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}"; globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
</script> </script>

View File

@@ -4,10 +4,12 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<meta http-equiv="x-ua-compatible" content="ie=edge" /> <meta http-equiv="x-ua-compatible" content="ie=edge" />
<title>Penpot - Render</title> <title>Penpot - Render</title>
<link rel="icon" href="images/favicon.png" />
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
<script> <script>
globalThis.penpotVersion = "{{& version}}"; globalThis.penpotVersion = "{{& version}}";
globalThis.penpotVersionTag = "{{& version_tag}}";
globalThis.penpotBuildDate = "{{& build_date}}"; globalThis.penpotBuildDate = "{{& build_date}}";
</script> </script>

View File

@@ -27,9 +27,11 @@ export function startWorker() {
}); });
} }
export const isDebug = process.env.NODE_ENV !== "production"; export const IS_DEBUG = process.env.NODE_ENV !== "production";
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop"; export const BUILD_DATE = process.env.BUILD_DATE || (new Date().toString()) ;
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date(); export const BUILD_TS = process.env.BUILD_TS || Date.now();
export const VERSION = process.env.VERSION || "develop";
export const VERSION_TAG = process.env.VERSION_TAG || VERSION;
async function findFiles(basePath, predicate, options = {}) { async function findFiles(basePath, predicate, options = {}) {
predicate = predicate =
@@ -193,25 +195,25 @@ async function generateManifest() {
render_main: "./js/render.js", render_main: "./js/render.js",
rasterizer_main: "./js/rasterizer.js", rasterizer_main: "./js/rasterizer.js",
config: "./js/config.js?version=" + CURRENT_VERSION, config: "./js/config.js?version=" + VERSION_TAG,
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION, polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
libs: "./js/libs.js?version=" + CURRENT_VERSION, libs: "./js/libs.js?version=" + VERSION_TAG,
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION, worker_main: "./js/worker/main.js?version=" + VERSION_TAG,
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION, default_translations: "./js/translation.en.js?version=" + VERSION_TAG,
importmap: JSON.stringify({ importmap: JSON.stringify({
"imports": { "imports": {
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION, "./js/shared.js": "./js/shared.js?version=" + VERSION_TAG,
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION, "./js/main.js": "./js/main.js?version=" + VERSION_TAG,
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION, "./js/render.js": "./js/render.js?version=" + VERSION_TAG,
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION, "./js/render-wasm.js": "./js/render-wasm.js?version=" + VERSION_TAG,
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION, "./js/rasterizer.js": "./js/rasterizer.js?version=" + VERSION_TAG,
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION, "./js/main-dashboard.js": "./js/main-dashboard.js?version=" + VERSION_TAG,
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION, "./js/main-auth.js": "./js/main-auth.js?version=" + VERSION_TAG,
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION, "./js/main-viewer.js": "./js/main-viewer.js?version=" + VERSION_TAG,
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION, "./js/main-settings.js": "./js/main-settings.js?version=" + VERSION_TAG,
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION, "./js/main-workspace.js": "./js/main-workspace.js?version=" + VERSION_TAG,
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION "./js/util-highlight.js": "./js/util-highlight.js?version=" + VERSION_TAG
} }
}) })
}; };
@@ -222,11 +224,12 @@ async function generateManifest() {
async function renderTemplate(path, context = {}, partials = {}) { async function renderTemplate(path, context = {}, partials = {}) {
const content = await fs.readFile(path, { encoding: "utf-8" }); const content = await fs.readFile(path, { encoding: "utf-8" });
const ts = Math.floor(new Date());
context = Object.assign({}, context, { context = Object.assign({}, context, {
ts: ts, isDebug: IS_DEBUG,
isDebug, version: VERSION,
version_tag: VERSION_TAG,
build_date: BUILD_DATE,
build_ts: BUILD_TS,
}); });
return mustache.render(content, context, partials); return mustache.render(content, context, partials);
@@ -390,7 +393,6 @@ async function generateSvgSprites() {
} }
async function generateTemplates() { async function generateTemplates() {
const isDebug = process.env.NODE_ENV !== "production";
await fs.mkdir("./resources/public/", { recursive: true }); await fs.mkdir("./resources/public/", { recursive: true });
const manifest = await generateManifest(); const manifest = await generateManifest();
@@ -415,10 +417,7 @@ async function generateTemplates() {
}; };
const context = { const context = {
manifest: manifest, manifest: manifest
version: CURRENT_VERSION,
build_date: BUILD_DATE,
isDebug,
}; };
content = await renderTemplate( content = await renderTemplate(
@@ -487,7 +486,7 @@ export async function compileStyles() {
await fs.mkdir("./resources/public/css", { recursive: true }); await fs.mkdir("./resources/public/css", { recursive: true });
await fs.writeFile("./resources/public/css/main.css", result); await fs.writeFile("./resources/public/css/main.css", result);
if (isDebug) { if (IS_DEBUG) {
let debugCSS = await compileSassDebug(worker); let debugCSS = await compileSassDebug(worker);
await fs.writeFile("./resources/public/css/debug.css", debugCSS); await fs.writeFile("./resources/public/css/debug.css", debugCSS);
} }

View File

@@ -2,26 +2,26 @@
# NOTE: this script should be called from the parent directory to # NOTE: this script should be called from the parent directory to
# properly work. # properly work.
set -ex
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no}; export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
export INCLUDE_WASM=${BUILD_WASM:-yes}; export INCLUDE_WASM=${BUILD_WASM:-yes};
export CURRENT_VERSION=$1;
export BUILD_DATE=$(date -R);
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS; export EXTRA_PARAMS=$SHADOWCLJS_EXTRA_PARAMS;
export TS=$(date +%s);
export BUILD_DATE=$(date -R);
export BUILD_TS=$(date +%s);
export VERSION=${1:-develop};
export VERSION_TAG="${VERSION}-${BUILD_TS}";
# Some cljs reacts on this environment variable for define more # Some cljs reacts on this environment variable for define more
# performant code on macros (example: rumext) # performant code on macros (example: rumext)
export NODE_ENV=production; export NODE_ENV=production;
echo "Current path:"
echo $PATH
set -ex
corepack enable; corepack enable;
corepack install; corepack install;
yarn install || exit 1; yarn install;
rm -rf target/dist; rm -rf target/dist;
rm -rf resources/public; rm -rf resources/public;
@@ -37,7 +37,7 @@ yarn run build:app:main $EXTRA_PARAMS;
yarn run build:app:libs; yarn run build:app:libs;
yarn run build:app:assets; yarn run build:app:assets;
sed -i "s/\.\/render.js/.\/render.js?version=$CURRENT_VERSION/g" resources/public/js/worker/main*.js sed -i "s/\.\/render.js/.\/render.js?version=$VERSION_TAG/g" resources/public/js/worker/main*.js
rsync -avr resources/public/ target/dist/ rsync -avr resources/public/ target/dist/

View File

@@ -2,18 +2,16 @@
# NOTE: this script should be called from the parent directory to # NOTE: this script should be called from the parent directory to
# properly work. # properly work.
export CURRENT_VERSION=$1; set -ex
export BUILD_TS=$(date +%s);
export BUILD_DATE=$(date -R); export BUILD_DATE=$(date -R);
export CURRENT_HASH=${CURRENT_HASH:-$(git rev-parse --short HEAD)};
export TS=$(date +%s); export VERSION=${1:-develop};
export VERSION_TAG="${VERSION}-${BUILD_TS}";
export NODE_ENV=production; export NODE_ENV=production;
echo "Current path:"
echo $PATH
set -ex
corepack enable; corepack enable;
corepack install || exit 1; corepack install || exit 1;
yarn install || exit 1; yarn install || exit 1;

View File

@@ -95,6 +95,7 @@
(def browser (parse-browser)) (def browser (parse-browser))
(def platform (parse-platform)) (def platform (parse-platform))
(def version-tag (obj/get global "penpotVersionTag"))
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI")) (def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI")) (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
@@ -190,9 +191,8 @@
(defn resolve-href (defn resolve-href
[resource] [resource]
(let [version (get version :full) (let [href (-> public-uri
href (-> public-uri (u/ensure-path-slash)
(u/ensure-path-slash) (u/join resource)
(u/join resource) (get :path))]
(get :path))] (str href "?version=" version-tag)))
(str href "?version=" version)))

View File

@@ -27,8 +27,10 @@
[app.main.data.workspace.colors :as wdc] [app.main.data.workspace.colors :as wdc]
[app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.store :as st] [app.main.store :as st]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
@@ -300,11 +302,20 @@
update-fn (fn [node _] update-fn (fn [node _]
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node)))] (cty/remove-typography-from-node)))
(dwsh/update-shapes shape-ids ;; Check if any attribute affects text layout (requires resize)
#(txt/update-text-content % update-node? update-fn nil) affects-layout? (some #(contains? txt-attrs %) [:font-size :font-family :font-weight :letter-spacing :line-height])]
{:ignore-touched true (ptk/reify ::generate-text-shape-update
:page-id page-id}))) ptk/WatchEvent
(watch [_ state _]
(cond-> (rx/of (dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id}))
(and affects-layout?
(features/active-feature? state "render-wasm/v1"))
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-line-height (defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil)) ([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
@@ -353,11 +364,17 @@
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))] (cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids (ptk/reify ::generate-font-family-text-shape-update
(fn [shape] ptk/WatchEvent
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil)) (watch [_ state _]
{:ignore-touched true (cond-> (rx/of (dwsh/update-shapes shape-ids
:page-id page-id}))) (fn [shape]
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
{:ignore-touched true
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn- create-font-family-text-attrs (defn- create-font-family-text-attrs
[value] [value]
@@ -425,10 +442,16 @@
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))] (cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids (ptk/reify ::generate-font-weight-text-shape-update
#(txt/update-text-content % update-node? update-fn nil) ptk/WatchEvent
{:ignore-touched true (watch [_ state _]
:page-id page-id}))) (cond-> (rx/of (dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-font-weight (defn update-font-weight
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil)) ([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))

View File

@@ -433,11 +433,22 @@
ptk/WatchEvent ptk/WatchEvent
(watch [it state _] (watch [it state _]
(let [data (dsh/lookup-file-data state) (let [data (dsh/lookup-file-data state)
changes (-> (pcb/empty-changes it) changes (-> (pcb/empty-changes it)
(pcb/with-library-data data) (pcb/with-library-data data)
(pcb/set-token set-id token-id nil))] (pcb/set-token set-id token-id nil))]
(rx/of (dch/commit-changes changes)))))) (rx/of (dch/commit-changes changes))))))
(defn bulk-delete-tokens
[set-id token-ids]
(dm/assert! (uuid? set-id))
(dm/assert! (every? uuid? token-ids))
(ptk/reify ::bulk-delete-tokens
ptk/WatchEvent
(watch [_ _ _]
(apply rx/of
(map #(delete-token set-id %) token-ids)))))
(defn duplicate-token (defn duplicate-token
[token-id] [token-id]
(dm/assert! (uuid? token-id)) (dm/assert! (uuid? token-id))
@@ -505,6 +516,19 @@
(update state :workspace-tokens assoc :token-context-menu params) (update state :workspace-tokens assoc :token-context-menu params)
(update state :workspace-tokens dissoc :token-context-menu))))) (update state :workspace-tokens dissoc :token-context-menu)))))
(defn assign-token-node-context-menu
[{:keys [position] :as params}]
(when params
(assert (gpt/point? position) "expected a point instance for `position` param"))
(ptk/reify ::show-token-node-context-menu
ptk/UpdateEvent
(update [_ state]
(if params
(update state :workspace-tokens assoc :token-node-context-menu params)
(update state :workspace-tokens dissoc :token-node-context-menu)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; TOKEN-SET UI OPS ;; TOKEN-SET UI OPS
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;

View File

@@ -483,6 +483,9 @@
(def workspace-active-theme-paths (def workspace-active-theme-paths
(l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib)) (l/derived (d/nilf ctob/get-active-theme-paths) tokens-lib))
(def workspace-all-tokens-map
(l/derived (d/nilf ctob/get-all-tokens-map) tokens-lib))
(defn token-sets-at-path-all-active (defn token-sets-at-path-all-active
[group-path] [group-path]
(l/derived (l/derived

View File

@@ -38,6 +38,7 @@
[app.main.ui.ds.product.loader :refer [loader*]] [app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.product.milestone :refer [milestone*]] [app.main.ui.ds.product.milestone :refer [milestone*]]
[app.main.ui.ds.product.milestone-group :refer [milestone-group*]] [app.main.ui.ds.product.milestone-group :refer [milestone-group*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.ds.storybook :as sb] [app.main.ui.ds.storybook :as sb]
[app.main.ui.ds.tooltip.tooltip :refer [tooltip*]] [app.main.ui.ds.tooltip.tooltip :refer [tooltip*]]
[app.main.ui.ds.utilities.date :refer [date*]] [app.main.ui.ds.utilities.date :refer [date*]]
@@ -81,6 +82,7 @@
:Milestone milestone* :Milestone milestone*
:MilestoneGroup milestone-group* :MilestoneGroup milestone-group*
:Date date* :Date date*
:PanelTitle panel-title*
:set-default-translations :set-default-translations
(fn [data] (fn [data]

View File

@@ -216,7 +216,7 @@
is-selected-on-focus nillable is-selected-on-focus nillable
tokens applied-token empty-to-end tokens applied-token empty-to-end
on-change on-blur on-focus on-detach on-change on-blur on-focus on-detach
property align ref] property align ref name]
:rest props}] :rest props}]
(let [;; NOTE: we use mfu/bean here for transparently handle (let [;; NOTE: we use mfu/bean here for transparently handle
@@ -662,7 +662,10 @@
label (get token :name) label (get token :name)
token-value (or (get token :resolved-value) token-value (or (get token :resolved-value)
(or (mf/ref-val last-value*) (or (mf/ref-val last-value*)
(fmt/format-number value)))] (fmt/format-number value)))
token-value (if (= name :opacity)
(* 100 token-value)
token-value)]
(mf/spread-props props (mf/spread-props props
{:id id {:id id
:label label :label label

View File

@@ -7,6 +7,7 @@
@use "ds/_borders.scss" as *; @use "ds/_borders.scss" as *;
@use "ds/_sizes.scss" as *; @use "ds/_sizes.scss" as *;
@use "ds/typography.scss" as *; @use "ds/typography.scss" as *;
@use "ds/_utils.scss" as *;
.option-list { .option-list {
--options-dropdown-icon-fg-color: var(--color-foreground-secondary); --options-dropdown-icon-fg-color: var(--color-foreground-secondary);
@@ -15,32 +16,32 @@
--options-dropdown-border-color: var(--color-background-quaternary); --options-dropdown-border-color: var(--color-background-quaternary);
position: absolute; position: absolute;
top: $sz-36; inset-block-start: $sz-36;
width: var(--dropdown-width, 100%); inline-size: var(--dropdown-width, 100%);
transform: translateX(var(--dropdown-translate-distance, 0)); transform: translateX(var(--dropdown-translate-distance, 0));
background-color: var(--options-dropdown-bg-color); background-color: var(--options-dropdown-bg-color);
border-radius: $br-8; border-radius: $br-8;
border: $b-1 solid var(--options-dropdown-border-color); border: $b-1 solid var(--options-dropdown-border-color);
padding-block: var(--sp-xs); padding-block: var(--sp-xs);
margin-block-end: 0; margin-block-end: 0;
max-height: $sz-400; max-block-size: $sz-400;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
z-index: var(--z-index-dropdown); z-index: var(--z-index-dropdown);
} }
.left-align { .left-align {
left: var(--dropdown-offset, 0); inset-inline-start: var(--dropdown-offset, 0);
} }
.right-align { .right-align {
right: var(--dropdown-offset, 0); inset-inline-end: var(--dropdown-offset, 0);
} }
.option-separator { .option-separator {
border: $b-1 solid var(--options-dropdown-border-color); border: $b-1 solid var(--options-dropdown-border-color);
margin-top: var(--sp-xs); margin-block-start: var(--sp-xs);
margin-bottom: var(--sp-xs); margin-block-end: var(--sp-xs);
} }
.group-option, .group-option,
@@ -51,11 +52,11 @@
gap: var(--sp-xs); gap: var(--sp-xs);
color: var(--color-foreground-secondary); color: var(--color-foreground-secondary);
padding-inline: var(--sp-s); padding-inline: var(--sp-s);
height: var(--sp-xxxl); block-size: var(--sp-xxxl);
} }
.option-empty { .option-empty {
justify-content: center; justify-content: center;
text-align: center; text-align: center;
padding: 0 40px; padding: 0 px2rem(40);
} }

View File

@@ -19,17 +19,19 @@
[:expandable {:optional true} :boolean] [:expandable {:optional true} :boolean]
[:expanded {:optional true} :boolean] [:expanded {:optional true} :boolean]
[:icon {:optional true} :string] [:icon {:optional true} :string]
[:on-toggle-expand fn?]]) [:on-toggle-expand {:optional true} fn?]
[:on-context-menu {:optional true} fn?]])
(mf/defc layer-button* (mf/defc layer-button*
{::mf/schema schema:layer-button} {::mf/schema schema:layer-button}
[{:keys [label description class is-expandable expanded icon on-toggle-expand children] :rest props}] [{:keys [label description class is-expandable expanded icon on-toggle-expand on-context-menu children] :rest props}]
(let [button-props (mf/spread-props props (let [button-props (mf/spread-props props
{:class [class (stl/css-case :layer-button true {:class [class (stl/css-case :layer-button true
:layer-button--expandable is-expandable :layer-button--expandable is-expandable
:layer-button--expanded expanded)] :layer-button--expanded expanded)]
:type "button" :type "button"
:on-click on-toggle-expand})] :on-click on-toggle-expand
:on-context-menu on-context-menu})]
[:div {:class (stl/css :layer-button-wrapper)} [:div {:class (stl/css :layer-button-wrapper)}
[:> "button" button-props [:> "button" button-props
[:div {:class (stl/css :layer-button-content)} [:div {:class (stl/css :layer-button-content)}

View File

@@ -0,0 +1,34 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.product.panel-title
(:require-macros
[app.main.style :as stl])
(:require
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
(def ^:private schema:panel-title
[:map
[:class {:optional true} :string]
[:text :string]
[:on-close {:optional true} fn?]])
(mf/defc panel-title*
{::mf/schema schema:panel-title}
[{:keys [class text on-close] :rest props}]
(let [props
(mf/spread-props props {:class [class (stl/css :panel-title)]})]
[:> :div props
[:span {:class (stl/css :panel-title-text)} text]
(when on-close
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click on-close
:icon i/close}])]))

View File

@@ -0,0 +1,26 @@
{ /* 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 */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as PanelTitle from "./panel_title.stories";
<Meta title="Product/PanelTitle" />
# PanelTitle
The `panel-title*` is used as a header for some sidebar sections.
<Canvas of={PanelTitle.Default} />
## Technical notes
The only mandatory parameter is `text`. Usually you'll want to pass a function property `on-close` that will be called when the user clicks on the close button on the right.
```clj
[:> panel-title* {:class class
:text text
:on-close on-close}]
```

View File

@@ -0,0 +1,25 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
.panel-title {
display: flex;
align-items: center;
justify-content: center;
block-size: $sz-32;
border-radius: $br-8;
background-color: var(--color-background-secondary);
}
.panel-title-text {
@include t.use-typography("headline-small");
flex-grow: 1;
text-align: center;
color: var(--color-foreground-primary);
}

View File

@@ -0,0 +1,21 @@
import * as React from "react";
import Components from "@target/components";
const { PanelTitle } = Components;
export default {
title: "Product/PanelTitle",
component: PanelTitle,
argTypes: {
text: {
control: { type: "text" },
},
},
args: {
text: "Lorem ipsum",
onClose: () => null,
},
render: ({ ...args }) => <PanelTitle {...args} />,
};
export const Default = {};

View File

@@ -23,6 +23,7 @@
touched? (and (contains? (:data @form) input-name) touched? (and (contains? (:data @form) input-name)
(get-in @form [:touched input-name])) (get-in @form [:touched input-name]))
error (get-in @form [:errors input-name]) error (get-in @form [:errors input-name])
value (get-in @form [:data input-name] "") value (get-in @form [:data input-name] "")
@@ -52,7 +53,8 @@
(let [form (mf/use-ctx context) (let [form (mf/use-ctx context)
disabled? (or (and (some? form) disabled? (or (and (some? form)
(or (not (:valid @form)) (or (not (:valid @form))
(seq (:external-errors @form)))) (seq (:async-errors @form))
(seq (:extra-errors @form))))
(true? disabled)) (true? disabled))
handle-key-down-save handle-key-down-save
(mf/use-fn (mf/use-fn

View File

@@ -312,8 +312,8 @@
[] []
(let [on-reload (mf/use-fn #(js/location.reload))] (let [on-reload (mf/use-fn #(js/location.reload))]
[:> error-container* {} [:> error-container* {}
[:div {:class (stl/css :main-message)} (tr "labels.webgl-context-lost.main-message")] [:div {:class (stl/css :main-message)} (tr "errors.webgl-context-lost.main-message")]
[:div {:class (stl/css :desc-message)} (tr "labels.webgl-context-lost.desc-message")] [:div {:class (stl/css :desc-message)} (tr "errors.webgl-context-lost.desc-message")]
[:div {:class (stl/css :buttons-container)} [:div {:class (stl/css :buttons-container)}
[:> button* {:variant "primary" :on-click on-reload} [:> button* {:variant "primary" :on-click on-reload}
(tr "labels.reload-page")]]])) (tr "labels.reload-page")]]]))

View File

@@ -276,7 +276,11 @@
:wglobal wglobal :wglobal wglobal
:layout layout}]) :layout layout}])
(when (or (not (and file-loaded? page-id)) (when (or (not (and file-loaded? page-id))
(and wasm-renderer-enabled? (not @first-frame-rendered?))) ;; in wasm renderer, extend the pixel loader until the first frame is rendered
;; but do not apply it when switching pages
(and wasm-renderer-enabled?
(not file-loaded?)
(not @first-frame-rendered?)))
[:> workspace-loader*])]]]]]])) [:> workspace-loader*])]]]]]]))
(mf/defc workspace-page* (mf/defc workspace-page*

View File

@@ -16,9 +16,9 @@
[app.main.ui.comments :as cmt] [app.main.ui.comments :as cmt]
[app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i] [app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.product.empty-state :refer [empty-state*]] [app.main.ui.ds.product.empty-state :refer [empty-state*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@@ -121,15 +121,12 @@
(st/emit! (with-meta (dcmt/open-thread thread) {::ev/origin "viewer"})) (st/emit! (with-meta (dcmt/open-thread thread) {::ev/origin "viewer"}))
(st/emit! (dwcm/navigate-to-comment thread)))))] (st/emit! (dwcm/navigate-to-comment thread)))))]
[:div {:class (stl/css-case :comments-section true [:div {:class (stl/css-case :comments-section true
:from-viewer from-viewer)} :from-viewer from-viewer)}
[:div {:class (stl/css-case :comments-section-title true
:viewer-title from-viewer)} [:> panel-title* {:class (stl/css :comments-title)
[:span (tr "labels.comments")] :text (tr "labels.comments")
[:> icon-button* {:variant "ghost" :on-close close-section}]
:aria-label (tr "labels.close")
:on-click close-section
:icon i/close}]]
[:button {:class (stl/css :mode-dropdown-wrapper) [:button {:class (stl/css :mode-dropdown-wrapper)
:on-click toggle-mode-selector} :on-click toggle-mode-selector}

View File

@@ -18,25 +18,8 @@
padding: 0 deprecated.$s-8; padding: 0 deprecated.$s-8;
} }
.comments-section-title { .comments-title {
@include deprecated.flexCenter; margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.viewer-title {
margin: 0;
margin-block-start: deprecated.$s-8;
} }
.mode-dropdown-wrapper { .mode-dropdown-wrapper {

View File

@@ -11,12 +11,11 @@
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(mf/defc debug-panel* (mf/defc debug-panel*
@@ -35,12 +34,9 @@
(st/emit! (dw/remove-layout-flag :debug-panel))))] (st/emit! (dw/remove-layout-flag :debug-panel))))]
[:div {:class (dm/str class " " (stl/css :debug-panel))} [:div {:class (dm/str class " " (stl/css :debug-panel))}
[:div {:class (stl/css :panel-title)} [:> panel-title* {:class (stl/css :debug-panel-title)
[:span "Debugging tools"] :text (tr "workspace.debug.title")
[:> icon-button* {:variant "ghost" :on-close handle-close}]
:aria-label (tr "labels.close")
:on-click handle-close
:icon i/close}]]
[:div {:class (stl/css :debug-panel-inner)} [:div {:class (stl/css :debug-panel-inner)}
(for [option (sort-by d/name dbg/options)] (for [option (sort-by d/name dbg/options)]

View File

@@ -12,21 +12,12 @@
background-color: var(--panel-background-color); background-color: var(--panel-background-color);
} }
.panel-title { .debug-panel-title {
@include deprecated.flexCenter; margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
@include deprecated.uppercaseTitleTipography; }
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span { .debug-panel-inner {
@include deprecated.flexCenter; padding: deprecated.$s-16 deprecated.$s-8;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
} }
.checkbox-wrapper { .checkbox-wrapper {
@@ -39,7 +30,3 @@
@extend .checkbox-icon; @extend .checkbox-icon;
cursor: pointer; cursor: pointer;
} }
.debug-panel-inner {
padding: deprecated.$s-16 deprecated.$s-8;
}

View File

@@ -13,7 +13,7 @@
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.ds.product.panel-title :refer [panel-title*]]
[debug :as dbg] [debug :as dbg]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@@ -125,11 +125,9 @@
(map (d/getf objects)))] (map (d/getf objects)))]
[:div {:class (stl/css :shape-info)} [:div {:class (stl/css :shape-info)}
[:div {:class (stl/css :shape-info-title)} [:> panel-title* {:class (stl/css :shape-info-title)
[:span "Debug"] :text "Debug"
[:div {:class (stl/css :close-button) :on-close #(dbg/disable! :shape-panel)}]
:on-click #(dbg/disable! :shape-panel)}
deprecated-icon/close]]
(if (empty? selected) (if (empty? selected)
[:div {:class (stl/css :attrs-container)} "No shapes selected"] [:div {:class (stl/css :attrs-container)} "No shapes selected"]

View File

@@ -16,34 +16,7 @@
} }
.shape-info-title { .shape-info-title {
@include deprecated.flexCenter; margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.close-button {
@extend .button-tertiary;
position: absolute;
right: deprecated.$s-2;
top: deprecated.$s-2;
height: deprecated.$s-28;
width: deprecated.$s-28;
border-radius: deprecated.$br-6;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
} }
.attrs-container { .attrs-container {

View File

@@ -13,23 +13,6 @@
background-color: var(--panel-background-color); background-color: var(--panel-background-color);
} }
.history-toolbox-title {
@include deprecated.flexCenter;
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
min-height: deprecated.$s-32;
margin: deprecated.$s-8 deprecated.$s-8 0 deprecated.$s-8;
border-radius: deprecated.$br-8;
background-color: var(--panel-title-background-color);
span {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
}
.history-entry-empty { .history-entry-empty {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -55,7 +55,6 @@
(-> (deref tokens) (-> (deref tokens)
(select-keys (get tk/tokens-by-input name)) (select-keys (get tk/tokens-by-input name))
(not-empty)))) (not-empty))))
on-detach-attr on-detach-attr
(mf/use-fn (mf/use-fn
(mf/deps on-detach name) (mf/deps on-detach name)

View File

@@ -9,13 +9,17 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
[app.common.types.token :as tk]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.tokens.application :as dwta]
[app.main.features :as features] [app.main.features :as features]
[app.main.store :as st] [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.select :refer [select]] [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.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.ds.foundations.assets.icon :as i]
[app.render-wasm.api :as wasm.api] [app.render-wasm.api :as wasm.api]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@@ -39,11 +43,16 @@
(defn- check-layer-menu-props (defn- check-layer-menu-props
[old-props new-props] [old-props new-props]
(let [old-values (unchecked-get old-props "values") (let [old-values (unchecked-get old-props "values")
new-values (unchecked-get new-props "values")] new-values (unchecked-get new-props "values")
old-applied-tokens (unchecked-get old-props "appliedTokens")
new-applied-tokens (unchecked-get new-props "appliedTokens")]
(and (identical? (unchecked-get old-props "class") (and (identical? (unchecked-get old-props "class")
(unchecked-get new-props "class")) (unchecked-get new-props "class"))
(identical? (unchecked-get old-props "ids") (identical? (unchecked-get old-props "ids")
(unchecked-get new-props "ids")) (unchecked-get new-props "ids"))
(identical? old-applied-tokens
new-applied-tokens)
(identical? (get old-values :opacity) (identical? (get old-values :opacity)
(get new-values :opacity)) (get new-values :opacity))
(identical? (get old-values :blend-mode) (identical? (get old-values :blend-mode)
@@ -53,12 +62,53 @@
(identical? (get old-values :hidden) (identical? (get old-values :hidden)
(get new-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/defc layer-menu*
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]} {::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
[{:keys [ids values]}] [{:keys [ids values applied-tokens]}]
(let [hidden? (get values :hidden) (let [token-numeric-inputs
(features/use-feature "tokens/numeric-input")
hidden? (get values :hidden)
blocked? (get values :blocked) blocked? (get values :blocked)
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}))))
current-blend-mode (or (get values :blend-mode) :normal) current-blend-mode (or (get values :blend-mode) :normal)
current-opacity (opacity->string (:opacity values)) current-opacity (opacity->string (:opacity values))
@@ -118,6 +168,17 @@
(let [value (/ value 100)] (let [value (/ value 100)]
(on-change ids :opacity value)))) (on-change ids :opacity value))))
on-opacity-change
(mf/use-fn
(mf/deps on-change handle-opacity-change)
(fn [value]
(if (or (string? value) (int? value))
(handle-opacity-change value)
(do
(st/emit! (dwta/toggle-token {:token (first value)
:attrs #{:opacity}
:shape-ids ids}))))))
handle-set-hidden handle-set-hidden
(mf/use-fn (mf/use-fn
(mf/deps ids) (mf/deps ids)
@@ -176,8 +237,9 @@
preview-complete?)) preview-complete?))
(swap! state* assoc :selected-blend-mode current-blend-mode))) (swap! state* assoc :selected-blend-mode current-blend-mode)))
[:div {:class (stl/css-case :element-set-content true [:section {:class (stl/css-case :element-set-content true
:hidden hidden?)} :hidden hidden?)
:aria-label "layer-menu-section"}
[:div {:class (stl/css :select)} [:div {:class (stl/css :select)}
[:& select [:& select
{:default-value selected-blend-mode {:default-value selected-blend-mode
@@ -187,16 +249,34 @@
:class (stl/css-case :hidden-select hidden?) :class (stl/css-case :hidden-select hidden?)
:on-pointer-enter-option handle-blend-mode-enter :on-pointer-enter-option handle-blend-mode-enter
:on-pointer-leave-option handle-blend-mode-leave}]] :on-pointer-leave-option handle-blend-mode-leave}]]
[:div {:class (stl/css :input)
:title (tr "workspace.options.opacity")}
[:span {:class (stl/css :icon)} "%"]
[:> numeric-input* (if token-numeric-inputs
{:value current-opacity
:placeholder "--" [:> numeric-input-wrapper*
:on-change handle-opacity-change {:on-change on-opacity-change
:min 0 :on-detach on-detach-token
:max 100 :icon i/percentage
:className (stl/css :numeric-input)}]] :min 0
:max 100
:name :opacity
:property (tr "workspace.options.opacity")
:applied-tokens applied-tokens
:align :right
:class (stl/css :numeric-input-wrapper)
:values values}]
[:div {:class (stl/css :input)
:title (tr "workspace.options.opacity")}
[:span {:class (stl/css :icon)} "%"]
[:> deprecated-input/numeric-input*
{:value current-opacity
:placeholder "--"
:on-change handle-opacity-change
:min 0
:max 100
:className (stl/css :numeric-input)}]])
[:div {:class (stl/css :actions)} [:div {:class (stl/css :actions)}

View File

@@ -6,6 +6,7 @@
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "../../../sidebar/common/sidebar.scss" as sidebar; @use "../../../sidebar/common/sidebar.scss" as sidebar;
@use "ds/_utils.scss" as *;
.element-set-content { .element-set-content {
@include sidebar.option-grid-structure; @include sidebar.option-grid-structure;
@@ -43,3 +44,9 @@
} }
} }
} }
.numeric-input-wrapper {
grid-column: span 2;
--dropdown-width: var(--7-columns-dropdown-width);
--dropdown-offset: #{px2rem(-35)};
}

View File

@@ -85,6 +85,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:type type :type type
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* {:ids ids [:> measures-menu* {:ids ids

View File

@@ -84,6 +84,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:type type :type type
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* {:ids ids [:> measures-menu* {:ids ids

View File

@@ -100,6 +100,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:type shape-type :type shape-type
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* {:ids ids [:> measures-menu* {:ids ids
:applied-tokens applied-tokens :applied-tokens applied-tokens

View File

@@ -111,6 +111,7 @@
[:div {:class (stl/css :options)} [:div {:class (stl/css :options)}
[:> layer-menu* {:type type [:> layer-menu* {:type type
:ids layer-ids :ids layer-ids
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* {:type type [:> measures-menu* {:type type
:ids measure-ids :ids measure-ids

View File

@@ -382,7 +382,7 @@
objects objects
objects))) objects)))
[layer-ids layer-values] [layer-ids layer-values layer-tokens]
(get-attrs shapes objects :layer) (get-attrs shapes objects :layer)
[text-ids text-values] [text-ids text-values]
@@ -406,7 +406,7 @@
[exports-ids exports-values] [exports-ids exports-values]
(get-attrs shapes objects :exports) (get-attrs shapes objects :exports)
[layout-container-ids layout-container-values layout-contianer-tokens] [layout-container-ids layout-container-values layout-container-tokens]
(get-attrs shapes objects :layout-container) (get-attrs shapes objects :layout-container)
[layout-item-ids layout-item-values {}] [layout-item-ids layout-item-values {}]
@@ -442,6 +442,7 @@
(when-not (empty? layer-ids) (when-not (empty? layer-ids)
[:> layer-menu* {:type type [:> layer-menu* {:type type
:ids layer-ids :ids layer-ids
:applied-tokens layer-tokens
:values layer-values}]) :values layer-values}])
(when-not (empty? measure-ids) (when-not (empty? measure-ids)
@@ -459,7 +460,7 @@
{:type type {:type type
:ids layout-container-ids :ids layout-container-ids
:values layout-container-values :values layout-container-values
:applied-tokens layout-contianer-tokens :applied-tokens layout-container-tokens
:multiple true}] :multiple true}]
(when (or is-layout-child? has-flex-layout-container?) (when (or is-layout-child? has-flex-layout-container?)

View File

@@ -84,6 +84,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:applied-tokens applied-tokens
:type type :type type
:values layer-values}] :values layer-values}]
[:> measures-menu* {:ids ids [:> measures-menu* {:ids ids

View File

@@ -85,6 +85,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:type type :type type
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* {:ids ids [:> measures-menu* {:ids ids
:type type :type type

View File

@@ -125,6 +125,7 @@
[:* [:*
[:> layer-menu* {:ids ids [:> layer-menu* {:ids ids
:type type :type type
:applied-tokens applied-tokens
:values layer-values}] :values layer-values}]
[:> measures-menu* [:> measures-menu*
{:ids ids {:ids ids

View File

@@ -18,8 +18,8 @@
[app.main.data.workspace.shortcuts] [app.main.data.workspace.shortcuts]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar*]] [app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]] [app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[app.util.strings :refer [matches-search]] [app.util.strings :refer [matches-search]]
@@ -487,13 +487,9 @@
(dom/focus! (dom/get-element "shortcut-search"))) (dom/focus! (dom/get-element "shortcut-search")))
[:div {:class (dm/str class " " (stl/css :shortcuts))} [:div {:class (dm/str class " " (stl/css :shortcuts))}
[:div {:class (stl/css :shortcuts-header)} [:> panel-title* {:class (stl/css :shortcuts-title)
[:div {:class (stl/css :shortcuts-title)} (tr "shortcuts.title")] :text (tr "shortcuts.title")
[:> icon-button* {:variant "ghost" :on-close close-fn}]
:icon i/close
:class (stl/css :shortcuts-close-button)
:on-click close-fn
:aria-label (tr "labels.close")}]]
[:div {:class (stl/css :search-field)} [:div {:class (stl/css :search-field)}
[:> search-bar* {:on-change on-search-term-change-2 [:> search-bar* {:on-change on-search-term-change-2

View File

@@ -18,27 +18,8 @@
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12; margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12;
} }
.shortcuts-header { .shortcuts-title {
@include deprecated.flexCenter; margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
@include deprecated.uppercaseTitleTipography;
position: relative;
height: deprecated.$s-32;
padding: deprecated.$s-2 deprecated.$s-2 deprecated.$s-2 0;
margin: deprecated.$s-4 deprecated.$s-4 0 deprecated.$s-4;
border-radius: deprecated.$br-6;
background-color: var(--panel-title-background-color);
.shortcuts-title {
@include deprecated.flexCenter;
flex-grow: 1;
color: var(--title-foreground-color-hover);
}
.shortcuts-close-button {
position: absolute;
right: 0;
top: 0;
}
} }
.section { .section {

View File

@@ -13,6 +13,7 @@
[app.main.data.helpers :as dsh] [app.main.data.helpers :as dsh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
[app.main.features :as features]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.title-bar :refer [title-bar*]] [app.main.ui.components.title-bar :refer [title-bar*]]
@@ -22,9 +23,11 @@
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.icons :as deprecated-icon] [app.main.ui.icons :as deprecated-icon]
[app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.badge :refer [badge-notification]]
[app.render-wasm.api :as wasm.api]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.timers :as timers]
[cuerdas.core :as str] [cuerdas.core :as str]
[okulary.core :as l] [okulary.core :as l]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
@@ -52,6 +55,8 @@
refs/workspace-data refs/workspace-data
=)) =))
;; --- Page Item ;; --- Page Item
(mf/defc page-item (mf/defc page-item
@@ -63,6 +68,22 @@
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id))) navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dcm/go-to-workspace :page-id id)))
read-only? (mf/use-ctx ctx/workspace-read-only?) read-only? (mf/use-ctx ctx/workspace-read-only?)
on-click
(mf/use-fn
(mf/deps id)
(fn []
;; when using the wasm renderer, apply a blur effect to the viewport canvas
(if (features/active-feature? @st/state "render-wasm/v1")
(do
(wasm.api/capture-canvas-pixels)
(wasm.api/apply-canvas-blur)
;; NOTE: it seems we need two RAF so the blur is actually applied and visible
;; in the canvas :(
(timers/raf
(fn []
(timers/raf navigate-fn))))
(navigate-fn))))
on-delete on-delete
(mf/use-fn (mf/use-fn
(mf/deps id) (mf/deps id)
@@ -155,7 +176,7 @@
:selected selected?) :selected selected?)
:data-testid (dm/str "page-" id) :data-testid (dm/str "page-" id)
:tab-index "0" :tab-index "0"
:on-click navigate-fn :on-click on-click
:on-double-click on-double-click :on-double-click on-double-click
:on-context-menu on-context-menu} :on-context-menu on-context-menu}
[:div {:class (stl/css :page-icon)} [:div {:class (stl/css :page-icon)}

View File

@@ -14,8 +14,10 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]] [app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]] [app.main.ui.workspace.tokens.management.context-menu :refer [token-context-menu]]
[app.main.ui.workspace.tokens.management.group :refer [token-group*]] [app.main.ui.workspace.tokens.management.group :refer [token-group*]]
[app.main.ui.workspace.tokens.management.node-context-menu :refer [token-node-context-menu*]]
[app.util.array :as array] [app.util.array :as array]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn- get-sorted-token-groups (defn- get-sorted-token-groups
@@ -120,7 +122,27 @@
[empty-group filled-group] [empty-group filled-group]
(mf/with-memo [tokens-by-type] (mf/with-memo [tokens-by-type]
(get-sorted-token-groups tokens-by-type))] (get-sorted-token-groups tokens-by-type))
;; Filter tokens by their path and return their ids
filter-tokens-by-path-ids
(mf/use-fn
(mf/deps tokens)
(fn [type path]
(->> tokens
(filter (fn [token]
(let [[_ token-value] token]
(and (= (:type token-value) type) (str/starts-with? (:name token-value) path)))))
(mapv (fn [token]
(let [[_ token-value] token]
(:id token-value)))))))
delete-node
(mf/with-memo [tokens selected-token-set-id]
(fn [node type]
(let [path (:path node)
tokens-in-path-ids (filter-tokens-by-path-ids type path)]
(st/emit! (dwtl/bulk-delete-tokens selected-token-set-id tokens-in-path-ids)))))]
(mf/with-effect [tokens-lib selected-token-set-id] (mf/with-effect [tokens-lib selected-token-set-id]
(when (and tokens-lib (when (and tokens-lib
@@ -134,6 +156,7 @@
[:* [:*
[:& token-context-menu] [:& token-context-menu]
[:> token-node-context-menu* {:on-delete-node delete-node}]
[:& selected-set-info* {:tokens-lib tokens-lib [:& selected-set-info* {:tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}] :selected-token-set-id selected-token-set-id}]

View File

@@ -140,6 +140,9 @@
error error
(get-in @form [:errors input-name]) (get-in @form [:errors input-name])
extra-error
(get-in @form [:extra-errors input-name])
value value
(get-in @form [:data input-name] "") (get-in @form [:data input-name] "")
@@ -247,9 +250,14 @@
:hint-type (:type hint)}) :hint-type (:type hint)})
props props
(if (and error touched?) (cond
(and error touched?)
(mf/spread-props props {:hint-type "error" (mf/spread-props props {:hint-type "error"
:hint-message (:message error)}) :hint-message (:message error)})
(and extra-error touched?)
(mf/spread-props props {:hint-type "error"
:hint-message (:message extra-error)})
:else
props)] props)]
(mf/with-effect [resolve-stream tokens token input-name] (mf/with-effect [resolve-stream tokens token input-name]

View File

@@ -236,12 +236,14 @@
(on-composite-input-change form field value false)) (on-composite-input-change form field value false))
([form field value trim?] ([form field value trim?]
(letfn [(clean-errors [errors] (letfn [(clean-errors [errors]
(-> errors (some-> errors
(dissoc field) (update :value #(when (map? %) (dissoc % field)))
(not-empty)))] (update :value #(when (seq %) %))
(not-empty)))]
(swap! form (fn [state] (swap! form (fn [state]
(-> state (-> state
(assoc-in [:data :value field] (if trim? (str/trim value) value)) (assoc-in [:data :value field] (if trim? (str/trim value) value))
(assoc-in [:touched :value field] true)
(update :errors clean-errors) (update :errors clean-errors)
(update :extra-errors clean-errors))))))) (update :extra-errors clean-errors)))))))
@@ -257,6 +259,9 @@
value value
(get-in @form [:data :value input-name] "") (get-in @form [:data :value input-name] "")
touched?
(get-in @form [:touched :value input-name])
resolve-stream resolve-stream
(mf/with-memo [token] (mf/with-memo [token]
(if-let [value (get-in token [:value input-name])] (if-let [value (get-in token [:value input-name])]
@@ -284,7 +289,7 @@
:hint-message (:message hint) :hint-message (:message hint)
:hint-type (:type hint)}) :hint-type (:type hint)})
props props
(if error (if (and touched? error)
(mf/spread-props props {:hint-type "error" (mf/spread-props props {:hint-type "error"
:hint-message (:message error)}) :hint-message (:message error)})
props) props)
@@ -332,6 +337,7 @@
message (tr "workspace.tokens.resolved-value" (or resolved-value value))] message (tr "workspace.tokens.resolved-value" (or resolved-value value))]
(swap! form update :errors dissoc :value) (swap! form update :errors dissoc :value)
(swap! form update :extra-errors dissoc :value) (swap! form update :extra-errors dissoc :value)
(swap! form update :async-errors dissoc :reference)
(if (= input-value (str resolved-value)) (if (= input-value (str resolved-value))
(reset! hint* {}) (reset! hint* {})
(reset! hint* {:message message :type "hint"})))))))] (reset! hint* {:message message :type "hint"})))))))]

View File

@@ -23,21 +23,19 @@
(let [token-type (let [token-type
(or (:type token) token-type) (or (:type token) token-type)
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
token-path token-path
(mf/with-memo [token] (mf/with-memo [token]
(cft/token-name->path (:name token))) (cft/token-name->path (:name token)))
tokens-tree-in-selected-set all-tokens (mf/deref refs/workspace-all-tokens-map)
(mf/with-memo [token-path tokens-in-selected-set]
(-> (ctob/tokens-tree tokens-in-selected-set) all-tokens
(mf/with-memo [token-path all-tokens]
(-> (ctob/tokens-tree all-tokens)
(d/dissoc-in token-path))) (d/dissoc-in token-path)))
props props
(mf/spread-props props {:token-type token-type (mf/spread-props props {:token-type token-type
:tokens-tree-in-selected-set tokens-tree-in-selected-set :all-token-tree all-tokens
:tokens-in-selected-set tokens-in-selected-set
:token token}) :token token})
text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")}) text-case-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-case-value-enter")})
text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")}) text-decoration-props (mf/spread-props props {:input-value-placeholder (tr "workspace.tokens.text-decoration-value-enter")})

View File

@@ -16,6 +16,7 @@
[app.main.data.helpers :as dh] [app.main.data.helpers :as dh]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.data.workspace.tokens.errors :as wte]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.data.workspace.tokens.propagation :as dwtp] [app.main.data.workspace.tokens.propagation :as dwtp]
[app.main.data.workspace.tokens.remapping :as remap] [app.main.data.workspace.tokens.remapping :as remap]
@@ -88,14 +89,13 @@
action action
is-create is-create
selected-token-set-id selected-token-set-id
tokens-tree-in-selected-set all-token-tree
token-type token-type
make-schema make-schema
input-component input-component
initial initial
type type
value-subfield value-subfield
tokens-in-selected-set
input-value-placeholder] :as props}] input-value-placeholder] :as props}]
(let [make-schema (or make-schema default-make-schema) (let [make-schema (or make-schema default-make-schema)
@@ -105,13 +105,6 @@
active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite)) active-tab* (mf/use-state #(if (cft/is-reference? token) :reference :composite))
active-tab (deref active-tab*) active-tab (deref active-tab*)
on-toggle-tab
(mf/use-fn
(mf/deps)
(fn [new-tab]
(let [new-tab (keyword new-tab)]
(reset! active-tab* new-tab))))
token token
(mf/with-memo [token] (mf/with-memo [token]
(or token {:type token-type})) (or token {:type token-type}))
@@ -124,6 +117,9 @@
tokens tokens
(mf/deref refs/workspace-active-theme-sets-tokens) (mf/deref refs/workspace-active-theme-sets-tokens)
tokens-in-selected-set
(mf/deref refs/workspace-all-tokens-in-selected-set)
tokens tokens
(mf/with-memo [tokens tokens-in-selected-set token] (mf/with-memo [tokens tokens-in-selected-set token]
;; Ensure that the resolved value uses the currently editing token ;; Ensure that the resolved value uses the currently editing token
@@ -134,8 +130,8 @@
(assoc (:name token) token))) (assoc (:name token) token)))
schema schema
(mf/with-memo [tokens-tree-in-selected-set active-tab] (mf/with-memo [all-token-tree active-tab]
(make-schema tokens-tree-in-selected-set active-tab)) (make-schema all-token-tree active-tab))
initial initial
(mf/with-memo [token] (mf/with-memo [token]
@@ -148,6 +144,17 @@
(fm/use-form :schema schema (fm/use-form :schema schema
:initial initial) :initial initial)
on-toggle-tab
(mf/use-fn
(mf/deps form)
(fn [new-tab]
(let [new-tab (keyword new-tab)]
(if (= new-tab :reference)
(swap! form assoc-in [:async-errors :reference]
{:message "Need valid reference"})
(swap! form update :async-errors dissoc :reference))
(reset! active-tab* new-tab))))
on-cancel on-cancel
(mf/use-fn (mf/use-fn
(fn [e] (fn [e]
@@ -224,7 +231,12 @@
:description description})) :description description}))
(dwtl/toggle-token-path path) (dwtl/toggle-token-path path)
(dwtp/propagate-workspace-tokens) (dwtp/propagate-workspace-tokens)
(modal/hide!))))))))))] (modal/hide!)))))
;; WORKAROUND: display validation errors in the form instead of crashing
(fn [{:keys [errors]}]
(let [error-messages (wte/humanize-errors errors)
error-message (first error-messages)]
(swap! form assoc-in [:extra-errors :value] {:message error-message}))))))))]
[:> fc/form* {:class (stl/css :form-wrapper) [:> fc/form* {:class (stl/css :form-wrapper)
:form form :form form

View File

@@ -291,6 +291,7 @@
[:color {:optional true} [:maybe :string]] [:color {:optional true} [:maybe :string]]
[:color-result {:optional true} ::sm/any] [:color-result {:optional true} ::sm/any]
[:inset {:optional true} [:maybe :boolean]]]]] [:inset {:optional true} [:maybe :boolean]]]]]
(if (= active-tab :reference) (if (= active-tab :reference)
[:reference {:optional false} ::sm/text] [:reference {:optional false} ::sm/text]
[:reference {:optional true} [:maybe :string]])]] [:reference {:optional true} [:maybe :string]])]]

View File

@@ -88,7 +88,7 @@
expandable? (d/nilv (seq tokens) false) expandable? (d/nilv (seq tokens) false)
on-context-menu on-pill-context-menu
(mf/use-fn (mf/use-fn
(fn [event token] (fn [event token]
(dom/prevent-default event) (dom/prevent-default event)
@@ -98,6 +98,15 @@
:errors (:errors token) :errors (:errors token)
:token-id (:id token)})))) :token-id (:id token)}))))
on-node-context-menu
(mf/use-fn
(fn [event node]
(dom/prevent-default event)
(st/emit! (dwtl/assign-token-node-context-menu
{:node node
:type type
:position (dom/get-client-position event)}))))
on-toggle-open-click on-toggle-open-click
(mf/use-fn (mf/use-fn
(mf/deps type expandable?) (mf/deps type expandable?)
@@ -159,4 +168,5 @@
:selected-token-set-id selected-token-set-id :selected-token-set-id selected-token-set-id
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:on-token-pill-click on-token-pill-click :on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu}])])) :on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu}])]))

View File

@@ -0,0 +1,83 @@
(ns app.main.ui.workspace.tokens.management.node-context-menu
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def ^:private schema:token-node-context-menu
[:map
[:on-delete-node fn?]])
(def ^:private tokens-node-menu-ref
(l/derived :token-node-context-menu refs/workspace-tokens))
(defn- prevent-default
[event]
(dom/prevent-default event)
(dom/stop-propagation event))
(mf/defc token-node-context-menu*
{::mf/schema schema:token-node-context-menu}
[{:keys [on-delete-node]}]
(let [mdata (mf/deref tokens-node-menu-ref)
is-open? (boolean mdata)
dropdown-ref (mf/use-ref)
dropdown-action (mf/use-ref)
dropdown-direction* (mf/use-state "down")
dropdown-direction (deref dropdown-direction*)
dropdown-direction-change* (mf/use-ref 0)
top (+ (get-in mdata [:position :y]) 5)
left (+ (get-in mdata [:position :x]) 5)
delete-node (mf/use-fn
(mf/deps mdata)
(fn []
(let [node (get mdata :node)
type (get mdata :type)]
(when node
(on-delete-node node type)))))]
(mf/with-effect [is-open?]
(when (and (not= 0 (mf/ref-val dropdown-direction-change*)) (= false is-open?))
(reset! dropdown-direction* "down")
(mf/set-ref-val! dropdown-direction-change* 0)))
(mf/with-effect [is-open? dropdown-ref dropdown-action]
(let [dropdown-element (mf/ref-val dropdown-ref)]
(when (and (= 0 (mf/ref-val dropdown-direction-change*)) dropdown-element)
(let [is-outside? (dom/is-element-outside? dropdown-element)]
(reset! dropdown-direction* (if is-outside? "up" "down"))
(mf/set-ref-val! dropdown-direction-change* (inc (mf/ref-val dropdown-direction-change*)))))))
;; FIXME: perf optimization
(when is-open?
(mf/portal
(mf/html
[:& dropdown {:show is-open?
:on-close #(st/emit! (dwtl/assign-token-node-context-menu nil))}
[:div {:class (stl/css :token-node-context-menu)
:data-testid "tokens-context-menu-for-token-node"
:ref dropdown-ref
:data-direction dropdown-direction
:style {:--bottom (if (= dropdown-direction "up")
"40px"
"unset")
:--top (dm/str top "px")
:left (dm/str left "px")}
:on-context-menu prevent-default}
(when mdata
[:ul {:class (stl/css :token-node-context-menu-list)}
[:li {:class (stl/css :token-node-context-menu-listitem)}
[:button {:class (stl/css :token-node-context-menu-action)
:type "button"
:on-click delete-node}
(tr "labels.delete")]]])]])
(dom/get-body)))))

View File

@@ -0,0 +1,70 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "ds/_utils.scss" as *;
@use "ds/_sizes.scss" as *;
@use "ds/_borders.scss" as *;
@use "ds/typography.scss" as t;
@use "ds/spacing.scss" as *;
@use "ds/mixins.scss" as *;
.token-node-context-menu {
--menu-inline-size: #{px2rem(240)};
position: absolute;
z-index: var(--z-index-dropdown);
}
.token-node-context-menu[data-direction="up"] {
bottom: var(--bottom);
}
.token-node-context-menu[data-direction="down"] {
top: var(--top);
}
.token-node-context-menu-list {
inline-size: var(--menu-inline-size);
padding: var(--sp-xs);
border-radius: $br-8;
border: $b-2 solid var(--color-background-quaternary);
background-color: var(--color-background-tertiary);
max-block-size: 100vh;
overflow-y: auto;
box-shadow: 0px 0px $sz-12 0px var(--menu-shadow-color);
}
.token-node-context-menu-action {
--context-menu-item-bg-color: none;
--context-menu-item-fg-color: var(--color-foreground-primary);
--context-menu-item-border-color: none;
@include t.use-typography("body-small");
appearance: none;
background: var(--context-menu-item-bg-color);
border: $b-1 solid var(--context-menu-item-border-color);
color: var(--context-menu-item-fg-color);
border-radius: $br-8;
cursor: pointer;
block-size: px2rem(32);
inline-size: 100%;
display: flex;
align-items: center;
padding: var(--sp-xs);
&:hover {
--context-menu-item-bg-color: var(--color-background-quaternary);
}
&:focus {
--context-menu-item-bg-color: var(--menu-background-color-focus);
--context-menu-item-border-color: var(--color-background-tertiary);
}
&[aria-selected="true"] {
--context-menu-item-bg-color: var(--color-background-quaternary);
}
}

View File

@@ -10,6 +10,7 @@
[app.common.path-names :as cpn] [app.common.path-names :as cpn]
[app.common.types.tokens-lib :as ctob] [app.common.types.tokens-lib :as ctob]
[app.main.data.workspace.tokens.library-edit :as dwtl] [app.main.data.workspace.tokens.library-edit :as dwtl]
[app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.ds.layers.layer-button :refer [layer-button*]] [app.main.ui.ds.layers.layer-button :refer [layer-button*]]
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]] [app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
@@ -26,7 +27,8 @@
[:selected-token-set-id {:optional true} :any] [:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any] [:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?] [:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]]) [:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(mf/defc folder-node* (mf/defc folder-node*
{::mf/schema schema:folder-node} {::mf/schema schema:folder-node}
@@ -39,22 +41,29 @@
selected-token-set-id selected-token-set-id
tokens-lib tokens-lib
on-token-pill-click on-token-pill-click
on-context-menu]}] on-pill-context-menu
on-node-context-menu]}]
(let [full-path (str (name type) "." (:path node)) (let [full-path (str (name type) "." (:path node))
is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path) is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
swap-folder-expanded (mf/use-fn swap-folder-expanded (mf/use-fn
(mf/deps (:path node) type) (mf/deps (:path node) type)
(fn [] (fn []
(let [path (str (name type) "." (:path node))] (let [path (str (name type) "." (:path node))]
(st/emit! (dwtl/toggle-token-path path)))))] (st/emit! (dwtl/toggle-token-path path)))))
node-context-menu-prep (mf/use-fn
(mf/deps on-node-context-menu node)
(fn [event]
(when on-node-context-menu
(on-node-context-menu event node))))]
[:li {:class (stl/css :folder-node)} [:li {:class (stl/css :folder-node)}
[:> layer-button* {:label (:name node) [:> layer-button* {:label (:name node)
:expanded is-folder-expanded :expanded is-folder-expanded
:aria-expanded is-folder-expanded :aria-expanded is-folder-expanded
:aria-controls (str "folder-children-" (:path node)) :aria-controls (str "folder-children-" (:path node))
:is-expandable (not (:leaf node)) :is-expandable (not (:leaf node))
:on-toggle-expand swap-folder-expanded}] :on-toggle-expand swap-folder-expanded
:on-context-menu node-context-menu-prep}]
(when is-folder-expanded (when is-folder-expanded
(let [children-fn (:children-fn node)] (let [children-fn (:children-fn node)]
[:div {:class (stl/css :folder-children-wrapper) [:div {:class (stl/css :folder-children-wrapper)
@@ -63,16 +72,17 @@
(let [children (children-fn)] (let [children (children-fn)]
(for [child children] (for [child children]
(if (not (:leaf child)) (if (not (:leaf child))
[:ul {:class (stl/css :node-parent)} [:ul {:class (stl/css :node-parent)
[:> folder-node* {:key (:path child) :key (:path child)}
:type type [:> folder-node* {:type type
:node child :node child
:unfolded-token-paths unfolded-token-paths :unfolded-token-paths unfolded-token-paths
:selected-shapes selected-shapes :selected-shapes selected-shapes
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:on-token-pill-click on-token-pill-click :on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu :on-pill-context-menu on-pill-context-menu
:on-node-context-menu on-node-context-menu
:tokens-lib tokens-lib :tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]] :selected-token-set-id selected-token-set-id}]]
(let [id (:id (:leaf child)) (let [id (:id (:leaf child))
@@ -84,7 +94,7 @@
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:on-click on-token-pill-click :on-click on-token-pill-click
:on-context-menu on-context-menu}])))))]))])) :on-context-menu on-pill-context-menu}])))))]))]))
(def ^:private schema:token-tree (def ^:private schema:token-tree
[:map [:map
@@ -97,7 +107,8 @@
[:selected-token-set-id {:optional true} :any] [:selected-token-set-id {:optional true} :any]
[:tokens-lib {:optional true} :any] [:tokens-lib {:optional true} :any]
[:on-token-pill-click {:optional true} fn?] [:on-token-pill-click {:optional true} fn?]
[:on-context-menu {:optional true} fn?]]) [:on-pill-context-menu {:optional true} fn?]
[:on-node-context-menu {:optional true} fn?]])
(mf/defc token-tree* (mf/defc token-tree*
{::mf/schema schema:token-tree} {::mf/schema schema:token-tree}
@@ -110,12 +121,19 @@
tokens-lib tokens-lib
selected-token-set-id selected-token-set-id
on-token-pill-click on-token-pill-click
on-context-menu]}] on-pill-context-menu
on-node-context-menu]}]
(let [separator "." (let [separator "."
tree (mf/use-memo tree (mf/use-memo
(mf/deps tokens) (mf/deps tokens)
(fn [] (fn []
(cpn/build-tree-root tokens separator)))] (cpn/build-tree-root tokens separator)))
can-edit? (:can-edit (deref refs/permissions))
on-node-context-menu (mf/use-fn
(mf/deps can-edit? on-node-context-menu)
(fn [event node]
(when can-edit?
(on-node-context-menu event node))))]
[:div {:class (stl/css :token-tree-wrapper)} [:div {:class (stl/css :token-tree-wrapper)}
(for [node tree] (for [node tree]
(if (:leaf node) (if (:leaf node)
@@ -127,7 +145,7 @@
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:on-click on-token-pill-click :on-click on-token-pill-click
:on-context-menu on-context-menu}]) :on-context-menu on-pill-context-menu}])
;; Render segment folder ;; Render segment folder
[:ul {:class (stl/css :node-parent) [:ul {:class (stl/css :node-parent)
:key (:path node)} :key (:path node)}
@@ -138,6 +156,7 @@
:is-selected-inside-layout is-selected-inside-layout :is-selected-inside-layout is-selected-inside-layout
:active-theme-tokens active-theme-tokens :active-theme-tokens active-theme-tokens
:on-token-pill-click on-token-pill-click :on-token-pill-click on-token-pill-click
:on-context-menu on-context-menu :on-node-context-menu on-node-context-menu
:on-pill-context-menu on-pill-context-menu
:tokens-lib tokens-lib :tokens-lib tokens-lib
:selected-token-set-id selected-token-set-id}]]))])) :selected-token-set-id selected-token-set-id}]]))]))

View File

@@ -20,7 +20,7 @@
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.combobox :refer [combobox*]] [app.main.ui.ds.controls.combobox :refer [combobox*]]
[app.main.ui.ds.controls.input :refer [input*]] [app.main.ui.ds.controls.input :refer [input*]]
[app.main.ui.ds.controls.radio-buttons :refer [radio-buttons*]] [app.main.ui.ds.controls.switch :refer [switch*]]
[app.main.ui.ds.controls.utilities.label :refer [label*]] [app.main.ui.ds.controls.utilities.label :refer [label*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.ds.foundations.typography.heading :refer [heading*]] [app.main.ui.ds.foundations.typography.heading :refer [heading*]]
@@ -85,21 +85,6 @@
:on-click create-theme} :on-click create-theme}
(tr "workspace.tokens.add-new-theme")]]]])) (tr "workspace.tokens.add-new-theme")]]]]))
(mf/defc switch*
[{:keys [selected? name on-change]}]
(let [selected (if selected? :on :off)]
[:> radio-buttons* {:selected selected
:on-change on-change
:name name
:options [{:id "on"
:icon i/tick
:label (tr "workspace.tokens.theme.enable")
:value :on}
{:id "off"
:icon i/close
:label (tr "workspace.tokens.theme.disable")
:value :off}]}]))
(mf/defc themes-overview (mf/defc themes-overview
[{:keys [change-view]}] [{:keys [change-view]}]
(let [active-theme-paths (mf/deref refs/workspace-active-theme-paths) (let [active-theme-paths (mf/deref refs/workspace-active-theme-paths)
@@ -137,6 +122,9 @@
(dom/prevent-default e) (dom/prevent-default e)
(dom/stop-propagation e) (dom/stop-propagation e)
(st/emit! (dwtl/delete-token-theme id))) (st/emit! (dwtl/delete-token-theme id)))
on-switch-theme
(fn []
(st/emit! (dwtl/toggle-token-theme-active id)))
on-edit-theme on-edit-theme
(fn [e] (fn [e]
(dom/prevent-default e) (dom/prevent-default e)
@@ -146,16 +134,10 @@
:class (stl/css :theme-row)} :class (stl/css :theme-row)}
[:div {:class (stl/css :theme-switch-row)} [:div {:class (stl/css :theme-switch-row)}
;; FIXME: FIREEEEEEEEEE THIS [:> switch* {:id name
[:div {:on-click (fn [e] :label name
(dom/prevent-default e) :on-change on-switch-theme
(dom/stop-propagation e) :default-checked selected?}]]
(st/emit! (dwtl/toggle-token-theme-active id)))}
[:> switch* {:name (tr "workspace.tokens.theme-name" name)
:on-change (constantly nil)
:selected? selected?}]]]
[:div {:class (stl/css :theme-name-row)}
[:> text* {:as "span" :typography "body-medium" :class (stl/css :theme-name) :title name} name]]
[:div {:class (stl/css :theme-actions-row)} [:div {:class (stl/css :theme-actions-row)}

View File

@@ -228,7 +228,7 @@
:class (stl/css :main-toolbar-options-button) :class (stl/css :main-toolbar-options-button)
:icon i/bug :icon i/bug
:aria-pressed (contains? layout :debug-panel) :aria-pressed (contains? layout :debug-panel)
:aria-label "Debugging tool" :aria-label (tr "workspace.toolbar.debug")
:tooltip-placement "bottom" :tooltip-placement "bottom"
:on-click toggle-debug-panel}]])]] :on-click toggle-debug-panel}]])]]

View File

@@ -312,6 +312,11 @@
(js/console.error "Error initializing canvas context:" e) (js/console.error "Error initializing canvas context:" e)
false))] false))]
(reset! canvas-init? init?) (reset! canvas-init? init?)
(when init?
;; Restore previous canvas pixels immediately after context initialization
;; This happens before initialize-viewport is called
(wasm.api/apply-canvas-blur)
(wasm.api/restore-previous-canvas-pixels))
(when-not init? (when-not init?
(js/alert "WebGL not supported") (js/alert "WebGL not supported")
(st/emit! (dcm/go-to-dashboard-recent)))))))) (st/emit! (dcm/go-to-dashboard-recent))))))))
@@ -340,6 +345,7 @@
(mf/with-effect [@canvas-init? zoom vbox background] (mf/with-effect [@canvas-init? zoom vbox background]
(when (and @canvas-init? (not @initialized?)) (when (and @canvas-init? (not @initialized?))
(wasm.api/clear-canvas-pixels)
(wasm.api/initialize-viewport base-objects zoom vbox background) (wasm.api/initialize-viewport base-objects zoom vbox background)
(reset! initialized? true))) (reset! initialized? true)))

View File

@@ -1185,7 +1185,6 @@
{:cmd :export-shapes {:cmd :export-shapes
:profile-id (:profile-id @st/state) :profile-id (:profile-id @st/state)
:wait true :wait true
:skip-children (:skip-children value false)
:exports [{:file-id file-id :exports [{:file-id file-id
:page-id page-id :page-id page-id
:object-id id :object-id id

View File

@@ -29,6 +29,7 @@
[app.main.worker :as mw] [app.main.worker :as mw]
[app.render-wasm.api.fonts :as f] [app.render-wasm.api.fonts :as f]
[app.render-wasm.api.texts :as t] [app.render-wasm.api.texts :as t]
[app.render-wasm.api.webgl :as webgl]
[app.render-wasm.deserializers :as dr] [app.render-wasm.deserializers :as dr]
[app.render-wasm.helpers :as h] [app.render-wasm.helpers :as h]
[app.render-wasm.mem :as mem] [app.render-wasm.mem :as mem]
@@ -37,7 +38,6 @@
[app.render-wasm.serializers :as sr] [app.render-wasm.serializers :as sr]
[app.render-wasm.serializers.color :as sr-clr] [app.render-wasm.serializers.color :as sr-clr]
[app.render-wasm.svg-filters :as svg-filters] [app.render-wasm.svg-filters :as svg-filters]
;; FIXME: rename; confunsing name
[app.render-wasm.wasm :as wasm] [app.render-wasm.wasm :as wasm]
[app.util.debug :as dbg] [app.util.debug :as dbg]
[app.util.dom :as dom] [app.util.dom :as dom]
@@ -279,30 +279,6 @@
[string] [string]
(+ (count string) 1)) (+ (count string) 1))
(defn- create-webgl-texture-from-image
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
[gl image-element]
(let [texture (.createTexture ^js gl)]
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
texture))
(defn- get-webgl-context
"Gets the WebGL context from the WASM module"
[]
(when wasm/context-initialized?
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
(when gl-obj
;; Get the current WebGL context from Emscripten
;; The GL object has a currentContext property that contains the context handle
(let [current-ctx (.-currentContext ^js gl-obj)]
(when current-ctx
(.-GLctx ^js current-ctx)))))))
(defn- get-texture-id-for-gl-object (defn- get-texture-id-for-gl-object
"Registers a WebGL texture with Emscripten's GL object system and returns its ID" "Registers a WebGL texture with Emscripten's GL object system and returns its ID"
@@ -332,8 +308,8 @@
(->> (retrieve-image url) (->> (retrieve-image url)
(rx/map (rx/map
(fn [img] (fn [img]
(when-let [gl (get-webgl-context)] (when-let [gl (webgl/get-webgl-context)]
(let [texture (create-webgl-texture-from-image gl img) (let [texture (webgl/create-webgl-texture-from-image gl img)
texture-id (get-texture-id-for-gl-object texture) texture-id (get-texture-id-for-gl-object texture)
width (.-width ^js img) width (.-width ^js img)
height (.-height ^js img) height (.-height ^js img)
@@ -979,6 +955,7 @@
(set-shape-grow-type grow-type)) (set-shape-grow-type grow-type))
(set-shape-layout shape) (set-shape-layout shape)
(set-layout-data shape)
(set-shape-selrect selrect) (set-shape-selrect selrect)
(let [pending_thumbnails (into [] (concat (let [pending_thumbnails (into [] (concat
@@ -1055,8 +1032,9 @@
(perf/end-measure "set-objects") (perf/end-measure "set-objects")
(process-pending shapes thumbnails full noop-fn (process-pending shapes thumbnails full noop-fn
(fn [] (fn []
(when render-callback (render-callback)) (if render-callback
(render-finish) (render-callback)
(render-finish))
(ug/dispatch! (ug/event "penpot:wasm:set-objects"))))))) (ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
(defn clear-focus-mode (defn clear-focus-mode
@@ -1384,8 +1362,9 @@
all-children all-children
(->> ids (->> ids
(mapcat #(cfh/get-children-with-self objects %)))] (mapcat #(cfh/get-children-with-self objects %)))]
(h/call wasm/internal-module "_init_shapes_pool" (count all-children)) (h/call wasm/internal-module "_init_shapes_pool" (count all-children))
(run! (partial set-object objects) all-children) (run! set-object all-children)
(let [content (-> (calculate-bool* bool-type ids) (let [content (-> (calculate-bool* bool-type ids)
(path.impl/path-data))] (path.impl/path-data))]
@@ -1448,6 +1427,12 @@
result))) result)))
(defn apply-canvas-blur
[]
(when wasm/canvas
(dom/set-style! wasm/canvas "filter" "blur(4px)")))
(defn init-wasm-module (defn init-wasm-module
[module] [module]
(let [default-fn (unchecked-get module "default") (let [default-fn (unchecked-get module "default")
@@ -1469,3 +1454,8 @@
(js/console.error cause) (js/console.error cause)
(p/resolved false))))) (p/resolved false)))))
(p/resolved false)))) (p/resolved false))))
;; Re-export public WebGL functions
(def capture-canvas-pixels webgl/capture-canvas-pixels)
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
(def clear-canvas-pixels webgl/clear-canvas-pixels)

View File

@@ -0,0 +1,166 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.render-wasm.api.webgl
"WebGL utilities for pixel capture and rendering"
(:require
[app.common.logging :as log]
[app.render-wasm.wasm :as wasm]
[app.util.dom :as dom]))
(defn get-webgl-context
"Gets the WebGL context from the WASM module"
[]
(when wasm/context-initialized?
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
(when gl-obj
;; Get the current WebGL context from Emscripten
;; The GL object has a currentContext property that contains the context handle
(let [current-ctx (.-currentContext ^js gl-obj)]
(when current-ctx
(.-GLctx ^js current-ctx)))))))
(defn create-webgl-texture-from-image
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
[gl image-element]
(let [texture (.createTexture ^js gl)]
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
texture))
;; FIXME: temporary function until we are able to keep the same <canvas> across pages.
(defn- draw-imagedata-to-webgl
"Draws ImageData to a WebGL2 context by creating a texture"
[gl image-data]
(let [width (.-width ^js image-data)
height (.-height ^js image-data)
texture (.createTexture ^js gl)]
;; Bind texture and set parameters
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-data)
;; Set up viewport
(.viewport ^js gl 0 0 width height)
;; Vertex & Fragment shaders
;; Since we are only calling this function once (on page switch), we don't need
;; to cache the compiled shaders somewhere else (cannot be reused in a differen context).
(let [vertex-shader-source "#version 300 es
in vec2 a_position;
in vec2 a_texCoord;
out vec2 v_texCoord;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
v_texCoord = a_texCoord;
}"
fragment-shader-source "#version 300 es
precision highp float;
in vec2 v_texCoord;
uniform sampler2D u_texture;
out vec4 fragColor;
void main() {
fragColor = texture(u_texture, v_texCoord);
}"
vertex-shader (.createShader ^js gl (.-VERTEX_SHADER ^js gl))
fragment-shader (.createShader ^js gl (.-FRAGMENT_SHADER ^js gl))
program (.createProgram ^js gl)]
(.shaderSource ^js gl vertex-shader vertex-shader-source)
(.compileShader ^js gl vertex-shader)
(when-not (.getShaderParameter ^js gl vertex-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Vertex shader compilation failed"
:log (.getShaderInfoLog ^js gl vertex-shader)))
(.shaderSource ^js gl fragment-shader fragment-shader-source)
(.compileShader ^js gl fragment-shader)
(when-not (.getShaderParameter ^js gl fragment-shader (.-COMPILE_STATUS ^js gl))
(log/error :hint "Fragment shader compilation failed"
:log (.getShaderInfoLog ^js gl fragment-shader)))
(.attachShader ^js gl program vertex-shader)
(.attachShader ^js gl program fragment-shader)
(.linkProgram ^js gl program)
(if (.getProgramParameter ^js gl program (.-LINK_STATUS ^js gl))
(do
(.useProgram ^js gl program)
;; Create full-screen quad vertices (normalized device coordinates)
(let [position-location (.getAttribLocation ^js gl program "a_position")
texcoord-location (.getAttribLocation ^js gl program "a_texCoord")
position-buffer (.createBuffer ^js gl)
texcoord-buffer (.createBuffer ^js gl)
positions #js [-1.0 -1.0 1.0 -1.0 -1.0 1.0 -1.0 1.0 1.0 -1.0 1.0 1.0]
texcoords #js [0.0 0.0 1.0 0.0 0.0 1.0 0.0 1.0 1.0 0.0 1.0 1.0]]
;; Set up position buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) position-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. positions) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl position-location)
(.vertexAttribPointer ^js gl position-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set up texcoord buffer
(.bindBuffer ^js gl (.-ARRAY_BUFFER ^js gl) texcoord-buffer)
(.bufferData ^js gl (.-ARRAY_BUFFER ^js gl) (js/Float32Array. texcoords) (.-STATIC_DRAW ^js gl))
(.enableVertexAttribArray ^js gl texcoord-location)
(.vertexAttribPointer ^js gl texcoord-location 2 (.-FLOAT ^js gl) false 0 0)
;; Set texture uniform
(.activeTexture ^js gl (.-TEXTURE0 ^js gl))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
(let [texture-location (.getUniformLocation ^js gl program "u_texture")]
(.uniform1i ^js gl texture-location 0))
;; draw
(.drawArrays ^js gl (.-TRIANGLES ^js gl) 0 6)
;; cleanup
(.deleteBuffer ^js gl position-buffer)
(.deleteBuffer ^js gl texcoord-buffer)
(.deleteShader ^js gl vertex-shader)
(.deleteShader ^js gl fragment-shader)
(.deleteProgram ^js gl program)))
(log/error :hint "Program linking failed"
:log (.getProgramInfoLog ^js gl program)))
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
(.deleteTexture ^js gl texture))))
(defn restore-previous-canvas-pixels
"Restores previous canvas pixels into the new canvas"
[]
(when-let [previous-canvas-pixels wasm/canvas-pixels]
(when-let [gl wasm/gl-context]
(draw-imagedata-to-webgl gl previous-canvas-pixels)
(set! wasm/canvas-pixels nil))))
(defn clear-canvas-pixels
[]
(when wasm/canvas
(let [context wasm/gl-context]
(.clearColor ^js context 0 0 0 0.0)
(.clear ^js context (.-COLOR_BUFFER_BIT ^js context))
(.clear ^js context (.-DEPTH_BUFFER_BIT ^js context))
(.clear ^js context (.-STENCIL_BUFFER_BIT ^js context)))
(dom/set-style! wasm/canvas "filter" "none")
(set! wasm/canvas-pixels nil)))
(defn capture-canvas-pixels
"Captures the pixels of the viewport canvas"
[]
(when wasm/canvas
(let [context wasm/gl-context
width (.-width wasm/canvas)
height (.-height wasm/canvas)
buffer (js/Uint8ClampedArray. (* width height 4))
_ (.readPixels ^js context 0 0 width height (.-RGBA ^js context) (.-UNSIGNED_BYTE ^js context) buffer)
image-data (js/ImageData. buffer width height)]
(set! wasm/canvas-pixels image-data))))

View File

@@ -12,6 +12,8 @@
;; Reference to the HTML canvas element. ;; Reference to the HTML canvas element.
(defonce canvas nil) (defonce canvas nil)
;; Reference to the captured pixels of the canvas (for page switching effect)
(defonce canvas-pixels nil)
;; Reference to the Emscripten GL context wrapper. ;; Reference to the Emscripten GL context wrapper.
(defonce gl-context-handle nil) (defonce gl-context-handle nil)
@@ -56,3 +58,4 @@
:stroke-linecap shared/RawStrokeLineCap :stroke-linecap shared/RawStrokeLineCap
:stroke-linejoin shared/RawStrokeLineJoin :stroke-linejoin shared/RawStrokeLineJoin
:fill-rule shared/RawFillRule}) :fill-rule shared/RawFillRule})

View File

@@ -114,7 +114,7 @@
(defn- load (defn- load
[locale] [locale]
(let [path (str "./translation." locale ".js?version=" (:full cf/version))] (let [path (str "./translation." locale ".js?version=" cf/version-tag)]
(->> (mod/import path) (->> (mod/import path)
(p/fmap (fn [result] (unchecked-get result "default"))) (p/fmap (fn [result] (unchecked-get result "default")))
(p/fnly (fn [data cause] (p/fnly (fn [data cause]

View File

@@ -179,6 +179,7 @@
(->> (render-canvas-blob canvas width height bgcolor) (->> (render-canvas-blob canvas width height bgcolor)
(p/fnly (fn [data cause] (p/fnly (fn [data cause]
(wasm.api/clear-canvas)
(if cause (if cause
(rx/error! subs cause) (rx/error! subs cause)
(rx/push! subs (rx/push! subs

View File

@@ -5476,6 +5476,10 @@ msgstr "Delete row and shapes"
msgid "workspace.context-menu.grid-track.row.duplicate" msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicate row" msgstr "Duplicate row"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512 #: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode" msgid "workspace.focus.focus-mode"
msgstr "Focus mode" msgstr "Focus mode"
@@ -8421,6 +8425,10 @@ msgstr "Comments (%s)"
msgid "workspace.toolbar.curve" msgid "workspace.toolbar.curve"
msgstr "Curve (%s)" msgstr "Curve (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/top_toolbar.cljs:172 #: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse" msgid "workspace.toolbar.ellipse"
msgstr "Ellipse (%s)" msgstr "Ellipse (%s)"
@@ -8731,10 +8739,10 @@ msgstr ""
msgid "workspace.versions.warning.text" msgid "workspace.versions.warning.text"
msgstr "Autosaved versions will be kept for %s days." msgstr "Autosaved versions will be kept for %s days."
msgid "labels.webgl-context-lost.main-message" msgid "errors.webgl-context-lost.main-message"
msgstr "Oops! The canvas context was lost" msgstr "Oops! The canvas context was lost"
msgid "labels.webgl-context-lost.desc-message" msgid "errors.webgl-context-lost.desc-message"
msgstr "WebGL has stopped working. Please reload the page to reset it" msgstr "WebGL has stopped working. Please reload the page to reset it"
#, unused #, unused

View File

@@ -5461,6 +5461,10 @@ msgstr "Borrar fila con el contenido"
msgid "workspace.context-menu.grid-track.row.duplicate" msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicar fila" msgstr "Duplicar fila"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512 #: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode" msgid "workspace.focus.focus-mode"
msgstr "Modo foco" msgstr "Modo foco"
@@ -7965,7 +7969,7 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
#: src/app/main/data/workspace/tokens/errors.cljs:57 #: src/app/main/data/workspace/tokens/errors.cljs:57
msgid "workspace.tokens.missing-references" msgid "workspace.tokens.missing-references"
msgstr "Referéncias de tokens no encontradas:" msgstr "Referencias de tokens no encontradas: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123 #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.more-options" msgid "workspace.tokens.more-options"
@@ -8282,6 +8286,10 @@ msgstr "Comentarios (%s)"
msgid "workspace.toolbar.curve" msgid "workspace.toolbar.curve"
msgstr "Curva (%s)" msgstr "Curva (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/top_toolbar.cljs:172 #: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse" msgid "workspace.toolbar.ellipse"
msgstr "Elipse (%s)" msgstr "Elipse (%s)"
@@ -8582,8 +8590,8 @@ msgstr "Los autoguardados duran %s días."
msgid "workspace.viewport.click-to-close-path" msgid "workspace.viewport.click-to-close-path"
msgstr "Pulsar para cerrar la ruta" msgstr "Pulsar para cerrar la ruta"
msgid "labels.webgl-context-lost.main-message" msgid "errors.webgl-context-lost.main-message"
msgstr "Ups! Se ha perdido el contexto del canvas" msgstr "Ups! Se ha perdido el contexto del canvas"
msgid "labels.webgl-context-lost.desc-message" msgid "errors.webgl-context-lost.desc-message"
msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo" msgstr "WebGL ha dejado de funcionar. Por favor, recarga la página para restaurarlo"

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env bash #!/usr/bin/env bash
export CURRENT_VERSION=${CURRENT_VERSION:-develop}; export VERSION_TAG=${VERSION:-develop};
if [ "$NODE_ENV" = "production" ]; then if [ "$NODE_ENV" = "production" ]; then
export BUILD_MODE="release"; export BUILD_MODE="release";
@@ -81,7 +81,7 @@ function copy_artifacts {
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js; cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js $DEST/$BUILD_NAME.js;
cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm; cp target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.wasm $DEST/$BUILD_NAME.wasm;
sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$CURRENT_VERSION/g" $DEST/$BUILD_NAME.js; sed -i "s/render_wasm.wasm/$BUILD_NAME.wasm?version=$VERSION_TAG/g" $DEST/$BUILD_NAME.js;
yarn esbuild target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js \ yarn esbuild target/wasm32-unknown-emscripten/$BUILD_MODE/render_wasm.js \
--log-level=error \ --log-level=error \

View File

@@ -284,6 +284,7 @@ pub extern "C" fn set_view_end() {
performance::end_measure!("set_view_end::clear_tile_index"); performance::end_measure!("set_view_end::clear_tile_index");
performance::end_timed_log!("clear_tile_index", _clear_start); performance::end_timed_log!("clear_tile_index", _clear_start);
} }
state.render_state.sync_cached_viewbox();
performance::end_measure!("set_view_end"); performance::end_measure!("set_view_end");
performance::end_timed_log!("set_view_end", _end_start); performance::end_timed_log!("set_view_end", _end_start);
#[cfg(feature = "profile-macros")] #[cfg(feature = "profile-macros")]

View File

@@ -1136,6 +1136,7 @@ impl RenderState {
) -> Result<(), String> { ) -> Result<(), String> {
let _start = performance::begin_timed_log!("start_render_loop"); let _start = performance::begin_timed_log!("start_render_loop");
let scale = self.get_scale(); let scale = self.get_scale();
self.tile_viewbox.update(self.viewbox, scale); self.tile_viewbox.update(self.viewbox, scale);
self.focus_mode.reset(); self.focus_mode.reset();
@@ -2292,6 +2293,10 @@ impl RenderState {
(self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON (self.viewbox.zoom - self.cached_viewbox.zoom).abs() > f32::EPSILON
} }
pub fn sync_cached_viewbox(&mut self) {
self.cached_viewbox = self.viewbox;
}
pub fn mark_touched(&mut self, uuid: Uuid) { pub fn mark_touched(&mut self, uuid: Uuid) {
self.touched_ids.insert(uuid); self.touched_ids.insert(uuid);
} }

View File

@@ -90,6 +90,18 @@ impl Type {
} }
} }
pub fn clear_corners(&mut self) {
match self {
Type::Rect(data) => {
data.corners = None;
}
Type::Frame(data) => {
data.corners = None;
}
_ => {}
}
}
pub fn path(&self) -> Option<&Path> { pub fn path(&self) -> Option<&Path> {
match self { match self {
Type::Path(path) => Some(path), Type::Path(path) => Some(path),
@@ -694,9 +706,11 @@ impl Shape {
pub fn set_corners(&mut self, raw_corners: (f32, f32, f32, f32)) { pub fn set_corners(&mut self, raw_corners: (f32, f32, f32, f32)) {
if let Some(corners) = make_corners(raw_corners) { if let Some(corners) = make_corners(raw_corners) {
self.shape_type.set_corners(corners); self.shape_type.set_corners(corners);
self.invalidate_bounds(); } else {
self.invalidate_extrect(); self.shape_type.clear_corners();
} }
self.invalidate_bounds();
self.invalidate_extrect();
} }
pub fn set_svg(&mut self, svg: skia::svg::Dom) { pub fn set_svg(&mut self, svg: skia::svg::Dom) {
@@ -1515,6 +1529,7 @@ impl Shape {
|| !self.transform.is_identity() || !self.transform.is_identity()
|| !math::is_close_to(self.rotation, 0.0) || !math::is_close_to(self.rotation, 0.0)
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_)) || matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|| matches!(self.shape_type, Type::Text(_))
} }
pub fn count_visible_inner_strokes(&self) -> usize { pub fn count_visible_inner_strokes(&self) -> usize {
@@ -1590,6 +1605,13 @@ mod tests {
} else { } else {
unreachable!(); unreachable!();
} }
shape.set_corners((0.0, 0.0, 0.0, 0.0));
if let Type::Rect(Rect { corners, .. }) = shape.shape_type {
assert_eq!(corners, None);
} else {
unreachable!();
}
} }
#[test] #[test]

View File

@@ -100,6 +100,16 @@ impl<'a> State<'a> {
} }
pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> { pub fn start_render_loop(&mut self, timestamp: i32) -> Result<(), String> {
// If zoom changed, we MUST rebuild the tile index before using it.
// Otherwise, the index will have tiles from the old zoom level, causing visible
// tiles to appear empty. This can happen if start_render_loop() is called before
// set_view_end() finishes rebuilding the index, or if set_view_end() hasn't been
// called yet.
let zoom_changed = self.render_state.zoom_changed();
if zoom_changed {
self.rebuild_tiles_shallow();
}
self.render_state self.render_state
.start_render_loop(None, &self.shapes, timestamp, false)?; .start_render_loop(None, &self.shapes, timestamp, false)?;
Ok(()) Ok(())