mirror of
https://github.com/penpot/penpot.git
synced 2026-01-21 12:50:11 -05:00
Compare commits
82 Commits
niwinz-sub
...
bameda-ci-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae458cccd0 | ||
|
|
b70eb768e0 | ||
|
|
b8c70be9a2 | ||
|
|
525adcfcbe | ||
|
|
7cce4c6532 | ||
|
|
a3fdd8b691 | ||
|
|
b6a9579c98 | ||
|
|
4397ede5c1 | ||
|
|
ff25df0457 | ||
|
|
8c7fd0af4b | ||
|
|
cf46051f56 | ||
|
|
079b3fbfad | ||
|
|
299f628951 | ||
|
|
32d0fe6463 | ||
|
|
6393330ee1 | ||
|
|
8252bc485e | ||
|
|
cecd3d4a90 | ||
|
|
1c2c0987f5 | ||
|
|
0418147e74 | ||
|
|
47775a9e2c | ||
|
|
7499a5bca6 | ||
|
|
6cd5bc76d7 | ||
|
|
8191d04114 | ||
|
|
bbe6ee2e19 | ||
|
|
fb6d8309b6 | ||
|
|
b7c2d9a079 | ||
|
|
aeb34a6f64 | ||
|
|
6fa0c3af0c | ||
|
|
260b9fb040 | ||
|
|
884954f4ff | ||
|
|
88f0f75174 | ||
|
|
1ffa956251 | ||
|
|
31054099ff | ||
|
|
689467bcf9 | ||
|
|
7724450037 | ||
|
|
368fa954ce | ||
|
|
983487d73c | ||
|
|
6fd0f5377c | ||
|
|
eb54bc485e | ||
|
|
12c24a36b4 | ||
|
|
324d54ad28 | ||
|
|
f42ff27f3d | ||
|
|
0ecb2bc838 | ||
|
|
3ecf509f3b | ||
|
|
2c1cc89f53 | ||
|
|
498b0b30fe | ||
|
|
89f40dcda2 | ||
|
|
e92f3fb3cb | ||
|
|
5193cfd56e | ||
|
|
7f395b2642 | ||
|
|
813d5d8e69 | ||
|
|
84f1ff092d | ||
|
|
2a62bd2586 | ||
|
|
ccac7bd510 | ||
|
|
f2b082b93e | ||
|
|
d73197625d | ||
|
|
43d1d127dc | ||
|
|
8bd3ef717c | ||
|
|
53bc647783 | ||
|
|
6029f9bb51 | ||
|
|
e0fd8bac81 | ||
|
|
34737ddfc9 | ||
|
|
a8dfd19338 | ||
|
|
e33e8a8c3b | ||
|
|
c411aefc6c | ||
|
|
311e124658 | ||
|
|
afc914f486 | ||
|
|
84f750da0d | ||
|
|
a3119bef5e | ||
|
|
c60d74df62 | ||
|
|
d593e299e3 | ||
|
|
4a8e02987f | ||
|
|
ee766e85a0 | ||
|
|
35e3b7f19a | ||
|
|
1810df232b | ||
|
|
3e99ad036c | ||
|
|
042a3a4080 | ||
|
|
f0687fd1f7 | ||
|
|
2c9159288f | ||
|
|
5c71c57dd9 | ||
|
|
5abc1aafb4 | ||
|
|
935728aa39 |
2
.github/workflows/build-bundle.yml
vendored
2
.github/workflows/build-bundle.yml
vendored
@@ -40,7 +40,7 @@ on:
|
||||
jobs:
|
||||
build-bundle:
|
||||
name: Build and Upload Penpot Bundle
|
||||
runs-on: ubuntu-24.04
|
||||
runs-on: penpot-runner-01
|
||||
env:
|
||||
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
|
||||
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
||||
|
||||
5
.github/workflows/build-docker-devenv.yml
vendored
5
.github/workflows/build-docker-devenv.yml
vendored
@@ -10,6 +10,11 @@ jobs:
|
||||
runs-on: penpot-runner-02
|
||||
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
|
||||
|
||||
14
.github/workflows/build-docker.yml
vendored
14
.github/workflows/build-docker.yml
vendored
@@ -22,6 +22,11 @@ jobs:
|
||||
runs-on: penpot-runner-02
|
||||
|
||||
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
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
@@ -66,6 +71,15 @@ jobs:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
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)
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
|
||||
73
.github/workflows/plugins-deploy-api-doc.yml
vendored
Normal file
73
.github/workflows/plugins-deploy-api-doc.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Plugins/api-doc deployer
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- develop
|
||||
- staging
|
||||
- main
|
||||
paths:
|
||||
- "plugins/**"
|
||||
- ".github/workflows/deploy-plugin-docs.yml"
|
||||
- "wrangle-penpot-plugins-api-doc.toml"
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
gh_ref:
|
||||
description: 'Name of the branch or ref'
|
||||
type: string
|
||||
required: true
|
||||
default: 'develop'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: plugins
|
||||
steps:
|
||||
- name: Extract some useful variables
|
||||
id: vars
|
||||
run: |
|
||||
echo "gh_ref=${{ inputs.gh_ref || github.ref_name }}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
ref: ${{ steps.vars.outputs.gh_ref }}
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
run_install: false
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install --frozen-lockfile
|
||||
|
||||
- name: Build docs
|
||||
run: pnpm run build:doc
|
||||
|
||||
- name: Select Worker name
|
||||
run: |
|
||||
REF="${{ steps.vars.outputs.gh_ref }}"
|
||||
case "$REF" in
|
||||
main) echo "WORKER_NAME=penpot-plugins-api-doc-pro" >> $GITHUB_ENV ;;
|
||||
staging) echo "WORKER_NAME=penpot-plugins-api-doc-pre" >> $GITHUB_ENV ;;
|
||||
develop) echo "WORKER_NAME=penpot-plugins-api-doc-hourly" >> $GITHUB_ENV ;;
|
||||
*) echo "Unsupported branch ${REF}" && exit 1 ;;
|
||||
esac
|
||||
|
||||
- name: Deploy to Cloudflare Workers
|
||||
uses: cloudflare/wrangler-action@v3
|
||||
with:
|
||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||
command: deploy --config wrangle-penpot-plugins-api-doc.toml --name ${{ env.WORKER_NAME }}
|
||||
40
.travis.yml
40
.travis.yml
@@ -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
|
||||
@@ -14,6 +14,8 @@
|
||||
- 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)
|
||||
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
|
||||
- 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
|
||||
|
||||
@@ -21,6 +23,8 @@
|
||||
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
|
||||
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
|
||||
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
|
||||
- Fix incorrect 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)
|
||||
|
||||
@@ -52,6 +56,7 @@
|
||||
- Fix missing text color token from selected shapes in selected colors list [Taiga #12956](https://tree.taiga.io/project/penpot/issue/12956)
|
||||
- Fix dropdown option width in Guides columns dropdown [Taiga #12959](https://tree.taiga.io/project/penpot/issue/12959)
|
||||
- Fix typos on download modal [Taiga #12865](https://tree.taiga.io/project/penpot/issue/12865)
|
||||
- Fix problem with text editor maintaining previous styles [Taiga #12835](https://tree.taiga.io/project/penpot/issue/12835)
|
||||
|
||||
## 2.12.1
|
||||
|
||||
@@ -167,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
|
||||
removed in future versions:
|
||||
|
||||
- The `PENPOT_ASSETS_STORAGE_BACKEND` becomes `PENPOT_OBJECTS_STORAGE_BACKEND` and its
|
||||
values passes from (`assets-fs` or `assets-s3`) to (`fs` or `s3`)
|
||||
- The `PENPOT_STORAGE_ASSETS_FS_DIRECTORY` becomes `PENPOT_OBJECTS_STORAGE_FS_DIRECTORY`
|
||||
|
||||
@@ -487,6 +487,7 @@
|
||||
:vertical-margin #{:spacing :dimensions}
|
||||
:sided-margins #{:spacing :dimensions}
|
||||
:line-height #{:line-height :number}
|
||||
:opacity #{:opacity}
|
||||
:font-size #{:font-size}
|
||||
:letter-spacing #{:letter-spacing}
|
||||
:fill #{:color}
|
||||
|
||||
@@ -223,7 +223,7 @@ http {
|
||||
add_header X-Cache-Status $upstream_cache_status;
|
||||
}
|
||||
|
||||
location ~* \.(jpg|png|svg|ttf|woff|woff2)$ {
|
||||
location ~* \.(jpg|png|svg|ttf|woff|woff2|gif)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
|
||||
@@ -152,9 +152,9 @@ services:
|
||||
|
||||
# AWS_ACCESS_KEY_ID: <KEY_ID>
|
||||
# AWS_SECRET_ACCESS_KEY: <ACCESS_KEY>
|
||||
# PENPOT_ASSETS_STORAGE_BACKEND: assets-s3
|
||||
# PENPOT_STORAGE_ASSETS_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_STORAGE_ASSETS_S3_BUCKET: <BUKET_NAME>
|
||||
# PENPOT_OBJECTS_STORAGE_BACKEND: s3
|
||||
# PENPOT_OBJECTS_STORAGE_S3_ENDPOINT: <ENDPOINT>
|
||||
# PENPOT_OBJECTS_STORAGE_S3_BUCKET: <BUKET_NAME>
|
||||
|
||||
## 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,
|
||||
|
||||
@@ -144,7 +144,7 @@ http {
|
||||
location / {
|
||||
include /etc/nginx/overrides/location.d/*.conf;
|
||||
|
||||
location ~* \.(js|css|wasm|jpg|png|map|svg|ttf|woff|woff2|)$ {
|
||||
location ~* \.(js|css|jpg|png|svg|gif|ttf|woff|woff2|wasm|map)$ {
|
||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||
}
|
||||
|
||||
|
||||
@@ -114,14 +114,7 @@ configuration.
|
||||
The callback has the following format:
|
||||
|
||||
```html
|
||||
https://<your_domain>/api/auth/oauth/<oauth_provider>/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
|
||||
https://<your_domain>/api/auth/oidc/callback
|
||||
```
|
||||
|
||||
#### Google
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"watch:app:main": "clojure -M:dev:shadow-cljs watch main worker storybook",
|
||||
"clear:shadow-cache": "rm -rf .shadow-cljs",
|
||||
"watch": "exit 0",
|
||||
"watch:app": "yarn run clear:shadow-cache && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:app": "yarn run clear:shadow-cache && yarn run build:wasm && concurrently --kill-others-on-fail \"yarn run watch:app:assets\" \"yarn run watch:app:main\" \"yarn run watch:app:libs\"",
|
||||
"watch:storybook": "yarn run build:storybook:assets && concurrently --kill-others-on-fail \"storybook dev -p 6006 --no-open\" \"node ./scripts/watch-storybook.js\""
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 25 KiB |
@@ -101,6 +101,70 @@ test.describe("Tokens: Apply token", () => {
|
||||
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 }) => {
|
||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTypographyTokensFile(page);
|
||||
@@ -129,189 +193,6 @@ test.describe("Tokens: Apply token", () => {
|
||||
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 ({
|
||||
page,
|
||||
}) => {
|
||||
|
||||
@@ -14,7 +14,7 @@ test.beforeEach(async ({ page }) => {
|
||||
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 }) => {
|
||||
await testTokenCreationFlow(page, {
|
||||
tokenLabel: "Border Radius",
|
||||
@@ -1008,6 +1008,41 @@ test.describe("Tokens - CRUD", () => {
|
||||
).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 }) => {
|
||||
const emptyNameError = "Name should be at least 1 character";
|
||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
@@ -1256,6 +1291,228 @@ test.describe("Tokens - CRUD", () => {
|
||||
).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 ({
|
||||
page,
|
||||
}) => {
|
||||
@@ -1388,67 +1645,7 @@ test.describe("Tokens - CRUD", () => {
|
||||
await expect(colorTokenChanged).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 changes color token color while keeping custom color space", async ({
|
||||
test("User edits color token color while keeping custom color space", async ({
|
||||
page,
|
||||
}) => {
|
||||
const { workspacePage, tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||
@@ -1502,30 +1699,9 @@ test.describe("Tokens - CRUD", () => {
|
||||
await valueSaturationSelector.click({ position: { x: 0, y: 0 } });
|
||||
await expect(valueField).toHaveValue(/^rgba(.*)$/);
|
||||
});
|
||||
});
|
||||
|
||||
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 - delete", () => {
|
||||
test("User delete color token", async ({ page }) => {
|
||||
const { tokensSidebar, tokenContextMenuForToken } =
|
||||
await setupTokensFile(page);
|
||||
@@ -1546,4 +1722,40 @@ test.describe("Tokens - CRUD", () => {
|
||||
await expect(tokenContextMenuForToken).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();
|
||||
});
|
||||
});
|
||||
|
||||
BIN
frontend/resources/images/features/2.13-shadow-tokens.gif
Normal file
BIN
frontend/resources/images/features/2.13-shadow-tokens.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 389 KiB |
BIN
frontend/resources/images/features/2.13-slide-0.jpg
Normal file
BIN
frontend/resources/images/features/2.13-slide-0.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 48 KiB |
BIN
frontend/resources/images/features/2.13-trash.gif
Normal file
BIN
frontend/resources/images/features/2.13-trash.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 284 KiB |
@@ -13,7 +13,7 @@
|
||||
$weight: unquote("normal"),
|
||||
$style: string.unquote("normal")
|
||||
) {
|
||||
$filepath: "../fonts/" + $file;
|
||||
$filepath: "/fonts/" + $file;
|
||||
|
||||
@font-face {
|
||||
font-family: "#{$style-name}";
|
||||
@@ -29,7 +29,7 @@
|
||||
}
|
||||
|
||||
@mixin font-face-variable($style-name, $file, $unicode-range) {
|
||||
$filepath: "../fonts/" + $file;
|
||||
$filepath: "/fonts/" + $file;
|
||||
|
||||
@font-face {
|
||||
font-family: "#{$style-name}";
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
|
||||
--app-background: var(--color-background-primary);
|
||||
--loader-background: var(--color-background-primary);
|
||||
--panel-title-background-color: var(--color-background-secondary);
|
||||
|
||||
// BUTTONS
|
||||
--button-foreground-hover: var(--color-accent-primary);
|
||||
|
||||
@@ -17,17 +17,18 @@
|
||||
<meta name="twitter:site" content="@penpotapp">
|
||||
<meta name="twitter:creator" content="@penpotapp">
|
||||
<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}}
|
||||
<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}}
|
||||
|
||||
<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="module">
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -3,10 +3,11 @@
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Penpot - Rasterizer</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
globalThis.penpotWorkerURI = "{{& manifest.worker_main}}";
|
||||
</script>
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
<meta charset="utf-8" />
|
||||
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||
<title>Penpot - Render</title>
|
||||
<link rel="icon" href="images/favicon.png" />
|
||||
|
||||
<link rel="icon" href="images/favicon.png?version={{& version_tag }}" />
|
||||
|
||||
<script>
|
||||
globalThis.penpotVersion = "{{& version}}";
|
||||
globalThis.penpotVersionTag = "{{& version_tag}}";
|
||||
globalThis.penpotBuildDate = "{{& build_date}}";
|
||||
</script>
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, draw_star,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, set_parent, allocBytes,
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, addShapeSolidFill, assignCanvas, hexToU32ARGB, getRandomInt, getRandomColor,
|
||||
getRandomFloat, useShape, setShapeChildren, setupInteraction, addShapeSolidStrokeFill
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
<body>
|
||||
<canvas id="canvas"></canvas>
|
||||
<script type="module">
|
||||
import initWasmModule from '/js/render_wasm.js';
|
||||
import initWasmModule from '/js/render-wasm.js';
|
||||
import {
|
||||
init, assignCanvas, setupInteraction, useShape, setShapeChildren, addTextShape, hexToU32ARGB,getRandomInt, getRandomColor, getRandomFloat, addShapeSolidFill, addShapeSolidStrokeFill
|
||||
} from './js/lib.js';
|
||||
@@ -102,4 +102,4 @@
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -27,9 +27,11 @@ export function startWorker() {
|
||||
});
|
||||
}
|
||||
|
||||
export const isDebug = process.env.NODE_ENV !== "production";
|
||||
export const CURRENT_VERSION = process.env.CURRENT_VERSION || "develop";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || "" + new Date();
|
||||
export const IS_DEBUG = process.env.NODE_ENV !== "production";
|
||||
export const BUILD_DATE = process.env.BUILD_DATE || (new Date().toString()) ;
|
||||
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 = {}) {
|
||||
predicate =
|
||||
@@ -172,6 +174,7 @@ export async function watch(baseDir, predicate, callback) {
|
||||
const watcher = new Watcher(baseDir, {
|
||||
persistent: true,
|
||||
recursive: true,
|
||||
debounce: 500
|
||||
});
|
||||
|
||||
watcher.on("change", (path) => {
|
||||
@@ -179,8 +182,19 @@ export async function watch(baseDir, predicate, callback) {
|
||||
callback(path);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
watcher.on("error", (cause) => {
|
||||
console.log("WATCHER ERROR", cause);
|
||||
});
|
||||
}
|
||||
|
||||
export async function ensureDirectories() {
|
||||
await fs.mkdir("./resources/public/js/worker/", { recursive: true });
|
||||
await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
}
|
||||
|
||||
|
||||
async function readManifestFile(resource) {
|
||||
const manifestPath = "resources/public/" + resource;
|
||||
let content = await fs.readFile(manifestPath, { encoding: "utf8" });
|
||||
@@ -193,25 +207,25 @@ async function generateManifest() {
|
||||
render_main: "./js/render.js",
|
||||
rasterizer_main: "./js/rasterizer.js",
|
||||
|
||||
config: "./js/config.js?version=" + CURRENT_VERSION,
|
||||
polyfills: "./js/polyfills.js?version=" + CURRENT_VERSION,
|
||||
libs: "./js/libs.js?version=" + CURRENT_VERSION,
|
||||
worker_main: "./js/worker/main.js?version=" + CURRENT_VERSION,
|
||||
default_translations: "./js/translation.en.js?version=" + CURRENT_VERSION,
|
||||
config: "./js/config.js?version=" + VERSION_TAG,
|
||||
polyfills: "./js/polyfills.js?version=" + VERSION_TAG,
|
||||
libs: "./js/libs.js?version=" + VERSION_TAG,
|
||||
worker_main: "./js/worker/main.js?version=" + VERSION_TAG,
|
||||
default_translations: "./js/translation.en.js?version=" + VERSION_TAG,
|
||||
|
||||
importmap: JSON.stringify({
|
||||
"imports": {
|
||||
"./js/shared.js": "./js/shared.js?version=" + CURRENT_VERSION,
|
||||
"./js/main.js": "./js/main.js?version=" + CURRENT_VERSION,
|
||||
"./js/render.js": "./js/render.js?version=" + CURRENT_VERSION,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + CURRENT_VERSION,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + CURRENT_VERSION,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + CURRENT_VERSION,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + CURRENT_VERSION
|
||||
"./js/shared.js": "./js/shared.js?version=" + VERSION_TAG,
|
||||
"./js/main.js": "./js/main.js?version=" + VERSION_TAG,
|
||||
"./js/render.js": "./js/render.js?version=" + VERSION_TAG,
|
||||
"./js/render-wasm.js": "./js/render-wasm.js?version=" + VERSION_TAG,
|
||||
"./js/rasterizer.js": "./js/rasterizer.js?version=" + VERSION_TAG,
|
||||
"./js/main-dashboard.js": "./js/main-dashboard.js?version=" + VERSION_TAG,
|
||||
"./js/main-auth.js": "./js/main-auth.js?version=" + VERSION_TAG,
|
||||
"./js/main-viewer.js": "./js/main-viewer.js?version=" + VERSION_TAG,
|
||||
"./js/main-settings.js": "./js/main-settings.js?version=" + VERSION_TAG,
|
||||
"./js/main-workspace.js": "./js/main-workspace.js?version=" + VERSION_TAG,
|
||||
"./js/util-highlight.js": "./js/util-highlight.js?version=" + VERSION_TAG
|
||||
}
|
||||
})
|
||||
};
|
||||
@@ -222,11 +236,12 @@ async function generateManifest() {
|
||||
async function renderTemplate(path, context = {}, partials = {}) {
|
||||
const content = await fs.readFile(path, { encoding: "utf-8" });
|
||||
|
||||
const ts = Math.floor(new Date());
|
||||
|
||||
context = Object.assign({}, context, {
|
||||
ts: ts,
|
||||
isDebug,
|
||||
isDebug: IS_DEBUG,
|
||||
version: VERSION,
|
||||
version_tag: VERSION_TAG,
|
||||
build_date: BUILD_DATE,
|
||||
build_ts: BUILD_TS,
|
||||
});
|
||||
|
||||
return mustache.render(content, context, partials);
|
||||
@@ -257,6 +272,9 @@ const markedOptions = {
|
||||
marked.use(markedOptions);
|
||||
|
||||
export async function compileTranslations() {
|
||||
const outputDir = "resources/public/js/";
|
||||
await fs.mkdir(outputDir, { recursive: true });
|
||||
|
||||
const langs = [
|
||||
"ar",
|
||||
"ca",
|
||||
@@ -338,7 +356,6 @@ export async function compileTranslations() {
|
||||
}
|
||||
|
||||
const esm = `export default ${JSON.stringify(result, null, 0)};\n`;
|
||||
const outputDir = "resources/public/js/";
|
||||
const outputFile = ph.join(outputDir, "translation." + lang + ".js");
|
||||
await fs.writeFile(outputFile, esm);
|
||||
}
|
||||
@@ -390,7 +407,6 @@ async function generateSvgSprites() {
|
||||
}
|
||||
|
||||
async function generateTemplates() {
|
||||
const isDebug = process.env.NODE_ENV !== "production";
|
||||
await fs.mkdir("./resources/public/", { recursive: true });
|
||||
|
||||
const manifest = await generateManifest();
|
||||
@@ -415,10 +431,7 @@ async function generateTemplates() {
|
||||
};
|
||||
|
||||
const context = {
|
||||
manifest: manifest,
|
||||
version: CURRENT_VERSION,
|
||||
build_date: BUILD_DATE,
|
||||
isDebug,
|
||||
manifest: manifest
|
||||
};
|
||||
|
||||
content = await renderTemplate(
|
||||
@@ -487,7 +500,7 @@ export async function compileStyles() {
|
||||
await fs.mkdir("./resources/public/css", { recursive: true });
|
||||
await fs.writeFile("./resources/public/css/main.css", result);
|
||||
|
||||
if (isDebug) {
|
||||
if (IS_DEBUG) {
|
||||
let debugCSS = await compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
}
|
||||
@@ -500,17 +513,43 @@ export async function compileStyles() {
|
||||
export async function compileSvgSprites() {
|
||||
const start = process.hrtime();
|
||||
log.info("init: compile svgsprite");
|
||||
await generateSvgSprites();
|
||||
let error = false;
|
||||
|
||||
try {
|
||||
await generateSvgSprites();
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile svgsprite", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile svgsprite", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function compileTemplates() {
|
||||
const start = process.hrtime();
|
||||
let error = false;
|
||||
log.info("init: compile templates");
|
||||
await generateTemplates();
|
||||
|
||||
try {
|
||||
await generateTemplates();
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile templates", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile templates", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile templates", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
export async function compilePolyfills() {
|
||||
|
||||
@@ -28,14 +28,12 @@ async function compileFile(path) {
|
||||
],
|
||||
sourceMap: false,
|
||||
});
|
||||
// console.dir(result);
|
||||
resolve({
|
||||
inputPath: path,
|
||||
outputPath: dest,
|
||||
css: result.css,
|
||||
});
|
||||
} catch (cause) {
|
||||
console.error(cause);
|
||||
reject(cause);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,26 +2,26 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
set -ex
|
||||
|
||||
export INCLUDE_STORYBOOK=${BUILD_STORYBOOK:-no};
|
||||
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 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
|
||||
# performant code on macros (example: rumext)
|
||||
export NODE_ENV=production;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install;
|
||||
yarn install || exit 1;
|
||||
yarn install;
|
||||
|
||||
rm -rf target/dist;
|
||||
rm -rf resources/public;
|
||||
@@ -37,7 +37,7 @@ yarn run build:app:main $EXTRA_PARAMS;
|
||||
yarn run build:app:libs;
|
||||
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/
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as h from "./_helpers.js";
|
||||
|
||||
await h.ensureDirectories();
|
||||
await h.compileStyles();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
|
||||
@@ -2,18 +2,16 @@
|
||||
# NOTE: this script should be called from the parent directory to
|
||||
# properly work.
|
||||
|
||||
export CURRENT_VERSION=$1;
|
||||
set -ex
|
||||
|
||||
export BUILD_TS=$(date +%s);
|
||||
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;
|
||||
|
||||
echo "Current path:"
|
||||
echo $PATH
|
||||
|
||||
set -ex
|
||||
|
||||
corepack enable;
|
||||
corepack install || exit 1;
|
||||
yarn install || exit 1;
|
||||
|
||||
@@ -12,19 +12,31 @@ let sass = null;
|
||||
|
||||
async function compileSassAll() {
|
||||
const start = process.hrtime();
|
||||
let error = false;
|
||||
|
||||
log.info("init: compile styles");
|
||||
|
||||
sass = await h.compileSassAll(worker);
|
||||
let output = await h.concatSass(sass);
|
||||
await fs.writeFile("./resources/public/css/main.css", output);
|
||||
try {
|
||||
sass = await h.compileSassAll(worker);
|
||||
let output = await h.concatSass(sass);
|
||||
await fs.writeFile("./resources/public/css/main.css", output);
|
||||
|
||||
if (isDebug) {
|
||||
let debugCSS = await h.compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
if (isDebug) {
|
||||
let debugCSS = await h.compileSassDebug(worker);
|
||||
await fs.writeFile("./resources/public/css/debug.css", debugCSS);
|
||||
}
|
||||
} catch (cause) {
|
||||
error = cause;
|
||||
}
|
||||
|
||||
const end = process.hrtime(start);
|
||||
log.info("done: compile styles", `(${ppt(end)})`);
|
||||
|
||||
if (error) {
|
||||
log.error("error: compile styles", `(${ppt(end)})`);
|
||||
console.error(error);
|
||||
} else {
|
||||
log.info("done: compile styles", `(${ppt(end)})`);
|
||||
}
|
||||
}
|
||||
|
||||
async function compileSass(path) {
|
||||
@@ -48,7 +60,7 @@ async function compileSass(path) {
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir("./resources/public/css/", { recursive: true });
|
||||
await h.ensureDirectories();
|
||||
await compileSassAll();
|
||||
await h.copyAssets();
|
||||
await h.copyWasmPlayground();
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
{:fn-invoke-direct true
|
||||
:optimizations #shadow/env ["PENPOT_BUILD_OPTIMIZATIONS" :as :keyword :default :advanced]
|
||||
:source-map true
|
||||
:pseudo-names true
|
||||
:elide-asserts true
|
||||
:anon-fn-naming-policy :off
|
||||
:cross-chunk-method-motion false
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
(def browser (parse-browser))
|
||||
(def platform (parse-platform))
|
||||
|
||||
(def version-tag (obj/get global "penpotVersionTag"))
|
||||
(def terms-of-service-uri (obj/get global "penpotTermsOfServiceURI"))
|
||||
(def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI"))
|
||||
(def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/"))
|
||||
@@ -110,9 +111,12 @@
|
||||
|
||||
(defn- normalize-uri
|
||||
[uri-str]
|
||||
;; Ensure that the path always ends with "/"; this ensures that
|
||||
;; all path join operations works as expected.
|
||||
(u/ensure-path-slash uri-str))
|
||||
(let [uri (u/uri uri-str)]
|
||||
;; Ensure that the path always ends with "/"; this ensures that
|
||||
;; all path join operations works as expected.
|
||||
(cond-> uri
|
||||
(not (str/ends-with? (:path uri) "/"))
|
||||
(update :path #(str % "/")))))
|
||||
|
||||
(def public-uri
|
||||
(normalize-uri (or (obj/get global "penpotPublicURI")
|
||||
@@ -187,9 +191,8 @@
|
||||
|
||||
(defn resolve-href
|
||||
[resource]
|
||||
(let [version (get version :full)
|
||||
href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version)))
|
||||
(let [href (-> public-uri
|
||||
(u/ensure-path-slash)
|
||||
(u/join resource)
|
||||
(get :path))]
|
||||
(str href "?version=" version-tag)))
|
||||
|
||||
@@ -27,8 +27,10 @@
|
||||
[app.main.data.workspace.colors :as wdc]
|
||||
[app.main.data.workspace.shape-layout :as dwsl]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.texts :as dwt]
|
||||
[app.main.data.workspace.transforms :as dwtr]
|
||||
[app.main.data.workspace.undo :as dwu]
|
||||
[app.main.features :as features]
|
||||
[app.main.fonts :as fonts]
|
||||
[app.main.store :as st]
|
||||
[app.util.i18n :refer [tr]]
|
||||
@@ -300,11 +302,20 @@
|
||||
update-fn (fn [node _]
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node)))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(cty/remove-typography-from-node)))
|
||||
;; Check if any attribute affects text layout (requires resize)
|
||||
affects-layout? (some #(contains? txt-attrs %) [:font-size :font-family :font-weight :letter-spacing :line-height])]
|
||||
(ptk/reify ::generate-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(and affects-layout?
|
||||
(features/active-feature? state "render-wasm/v1"))
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-line-height
|
||||
([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
|
||||
@@ -353,11 +364,17 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(ptk/reify ::generate-font-family-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
(fn [shape]
|
||||
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn- create-font-family-text-attrs
|
||||
[value]
|
||||
@@ -425,10 +442,16 @@
|
||||
(-> node
|
||||
(d/txt-merge txt-attrs)
|
||||
(cty/remove-typography-from-node))))]
|
||||
(dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id})))
|
||||
(ptk/reify ::generate-font-weight-text-shape-update
|
||||
ptk/WatchEvent
|
||||
(watch [_ state _]
|
||||
(cond-> (rx/of (dwsh/update-shapes shape-ids
|
||||
#(txt/update-text-content % update-node? update-fn nil)
|
||||
{:ignore-touched true
|
||||
:page-id page-id}))
|
||||
(features/active-feature? state "render-wasm/v1")
|
||||
(rx/merge
|
||||
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
|
||||
|
||||
(defn update-font-weight
|
||||
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
|
||||
|
||||
@@ -433,11 +433,22 @@
|
||||
ptk/WatchEvent
|
||||
(watch [it state _]
|
||||
(let [data (dsh/lookup-file-data state)
|
||||
|
||||
changes (-> (pcb/empty-changes it)
|
||||
(pcb/with-library-data data)
|
||||
(pcb/set-token set-id token-id nil))]
|
||||
(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
|
||||
[token-id]
|
||||
(dm/assert! (uuid? token-id))
|
||||
@@ -505,6 +516,19 @@
|
||||
(update state :workspace-tokens assoc :token-context-menu params)
|
||||
(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
|
||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uri :as u]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.globals :as globals]
|
||||
@@ -20,6 +19,7 @@
|
||||
[app.util.object :as obj]
|
||||
[beicon.v2.core :as rx]
|
||||
[cuerdas.core :as str]
|
||||
[lambdaisland.uri :as u]
|
||||
[okulary.core :as l]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -138,13 +138,13 @@
|
||||
"&display=block")]
|
||||
(dm/str
|
||||
(-> cf/public-uri
|
||||
(u/join "internal/gfonts/css")
|
||||
(assoc :path "/internal/gfonts/css")
|
||||
(assoc :query query)))))
|
||||
|
||||
(defn- process-gfont-css
|
||||
[css]
|
||||
(let [base (u/join cf/public-uri "internal/gfonts/font")]
|
||||
(str/replace css "https://fonts.gstatic.com/s" (dm/str base))))
|
||||
(let [base (dm/str (assoc cf/public-uri :path "/internal/gfonts/font"))]
|
||||
(str/replace css "https://fonts.gstatic.com/s" base)))
|
||||
|
||||
(defn- fetch-gfont-css
|
||||
[url]
|
||||
@@ -178,9 +178,7 @@
|
||||
|
||||
(defn- asset-id->uri
|
||||
[asset-id]
|
||||
(-> cf/public-uri
|
||||
(u/join "assets/by-id/" asset-id)
|
||||
(str)))
|
||||
(str (u/join cf/public-uri "assets/by-id/" asset-id)))
|
||||
|
||||
(defn generate-custom-font-variant-css
|
||||
[family variant]
|
||||
@@ -372,7 +370,7 @@
|
||||
:else
|
||||
(let [{:keys [weight style suffix]} (get-variant font font-variant-id)
|
||||
suffix (or suffix font-variant-id)
|
||||
params {:uri (str (u/join cf/public-uri (str "fonts/" family "-" suffix ".woff")))
|
||||
params {:uri (dm/str cf/public-uri "fonts/" family "-" suffix ".woff")
|
||||
:family family
|
||||
:style style
|
||||
:weight weight}]
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.logging :as log]
|
||||
[app.common.uri :as u]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.util.dom :as dom]
|
||||
@@ -27,9 +26,7 @@
|
||||
(defonce instance nil)
|
||||
(defonce msgbus (rx/subject))
|
||||
(defonce origin
|
||||
(-> cf/rasterizer-uri
|
||||
(u/join "rasterizer.html")
|
||||
(dm/str)))
|
||||
(dm/str (assoc cf/rasterizer-uri :path "/rasterizer.html")))
|
||||
|
||||
(declare send-message!)
|
||||
|
||||
@@ -132,9 +129,7 @@
|
||||
(dom/append-child! js/document.body iframe)
|
||||
(set! instance iframe))
|
||||
|
||||
(let [new-origin (-> cf/public-uri
|
||||
(u/join "rasterizer.html")
|
||||
(dm/str))]
|
||||
(let [new-origin (dm/str (assoc cf/public-uri :path "/rasterizer.html"))]
|
||||
(log/warn :hint "fallback to main domain" :origin new-origin)
|
||||
|
||||
(dom/set-attribute! iframe "src" new-origin)
|
||||
|
||||
@@ -483,6 +483,9 @@
|
||||
(def workspace-active-theme-paths
|
||||
(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
|
||||
[group-path]
|
||||
(l/derived
|
||||
|
||||
@@ -193,11 +193,11 @@
|
||||
restore-fn
|
||||
(fn [_]
|
||||
(st/emit! (dd/restore-files-immediately
|
||||
(with-meta {:team-id (:id current-team)
|
||||
(with-meta {:team-id current-team-id
|
||||
:ids (into #{} d/xf:map-id files)}
|
||||
{:on-success #(st/emit! (ntf/success (tr "dashboard.restore-success-notification" (:name file)))
|
||||
(dd/fetch-projects (:id current-team))
|
||||
(dd/fetch-deleted-files (:id current-team)))
|
||||
(dd/fetch-projects current-team-id)
|
||||
(dd/fetch-deleted-files current-team-id))
|
||||
:on-error #(st/emit! (ntf/error (tr "dashboard.errors.error-on-restore-file" (:name file))))}))))
|
||||
|
||||
on-restore-immediately
|
||||
@@ -214,7 +214,7 @@
|
||||
on-delete-immediately
|
||||
(fn []
|
||||
(let [accept-fn #(st/emit! (dd/delete-files-immediately
|
||||
{:team-id (:id current-team)
|
||||
{:team-id current-team-id
|
||||
:ids (into #{} d/xf:map-id files)}))]
|
||||
(st/emit!
|
||||
(modal/show {:type :confirm
|
||||
@@ -244,8 +244,7 @@
|
||||
(for [project current-projects]
|
||||
{:name (get-project-name project)
|
||||
:id (get-project-id project)
|
||||
:handler (on-move (:id current-team)
|
||||
(:id project))})
|
||||
:handler (on-move current-team-id (:id project))})
|
||||
(when (seq other-teams)
|
||||
[{:name (tr "dashboard.move-to-other-team")
|
||||
:id "move-to-other-team"
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
[app.main.ui.ds.product.loader :refer [loader*]]
|
||||
[app.main.ui.ds.product.milestone :refer [milestone*]]
|
||||
[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.tooltip.tooltip :refer [tooltip*]]
|
||||
[app.main.ui.ds.utilities.date :refer [date*]]
|
||||
@@ -81,6 +82,7 @@
|
||||
:Milestone milestone*
|
||||
:MilestoneGroup milestone-group*
|
||||
:Date date*
|
||||
:PanelTitle panel-title*
|
||||
|
||||
:set-default-translations
|
||||
(fn [data]
|
||||
|
||||
@@ -216,7 +216,7 @@
|
||||
is-selected-on-focus nillable
|
||||
tokens applied-token empty-to-end
|
||||
on-change on-blur on-focus on-detach
|
||||
property align ref]
|
||||
property align ref name]
|
||||
:rest props}]
|
||||
|
||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||
@@ -662,7 +662,10 @@
|
||||
label (get token :name)
|
||||
token-value (or (get token :resolved-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
|
||||
{:id id
|
||||
:label label
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
@use "ds/_borders.scss" as *;
|
||||
@use "ds/_sizes.scss" as *;
|
||||
@use "ds/typography.scss" as *;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.option-list {
|
||||
--options-dropdown-icon-fg-color: var(--color-foreground-secondary);
|
||||
@@ -15,32 +16,32 @@
|
||||
--options-dropdown-border-color: var(--color-background-quaternary);
|
||||
|
||||
position: absolute;
|
||||
top: $sz-36;
|
||||
width: var(--dropdown-width, 100%);
|
||||
inset-block-start: $sz-36;
|
||||
inline-size: var(--dropdown-width, 100%);
|
||||
transform: translateX(var(--dropdown-translate-distance, 0));
|
||||
background-color: var(--options-dropdown-bg-color);
|
||||
border-radius: $br-8;
|
||||
border: $b-1 solid var(--options-dropdown-border-color);
|
||||
padding-block: var(--sp-xs);
|
||||
margin-block-end: 0;
|
||||
max-height: $sz-400;
|
||||
max-block-size: $sz-400;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
z-index: var(--z-index-dropdown);
|
||||
}
|
||||
|
||||
.left-align {
|
||||
left: var(--dropdown-offset, 0);
|
||||
inset-inline-start: var(--dropdown-offset, 0);
|
||||
}
|
||||
|
||||
.right-align {
|
||||
right: var(--dropdown-offset, 0);
|
||||
inset-inline-end: var(--dropdown-offset, 0);
|
||||
}
|
||||
|
||||
.option-separator {
|
||||
border: $b-1 solid var(--options-dropdown-border-color);
|
||||
margin-top: var(--sp-xs);
|
||||
margin-bottom: var(--sp-xs);
|
||||
margin-block-start: var(--sp-xs);
|
||||
margin-block-end: var(--sp-xs);
|
||||
}
|
||||
|
||||
.group-option,
|
||||
@@ -51,11 +52,11 @@
|
||||
gap: var(--sp-xs);
|
||||
color: var(--color-foreground-secondary);
|
||||
padding-inline: var(--sp-s);
|
||||
height: var(--sp-xxxl);
|
||||
block-size: var(--sp-xxxl);
|
||||
}
|
||||
|
||||
.option-empty {
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 0 40px;
|
||||
padding: 0 px2rem(40);
|
||||
}
|
||||
|
||||
@@ -19,17 +19,19 @@
|
||||
[:expandable {:optional true} :boolean]
|
||||
[:expanded {:optional true} :boolean]
|
||||
[: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/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
|
||||
{:class [class (stl/css-case :layer-button true
|
||||
:layer-button--expandable is-expandable
|
||||
:layer-button--expanded expanded)]
|
||||
: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)}
|
||||
[:> "button" button-props
|
||||
[:div {:class (stl/css :layer-button-content)}
|
||||
|
||||
34
frontend/src/app/main/ui/ds/product/panel_title.cljs
Normal file
34
frontend/src/app/main/ui/ds/product/panel_title.cljs
Normal 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}])]))
|
||||
26
frontend/src/app/main/ui/ds/product/panel_title.mdx
Normal file
26
frontend/src/app/main/ui/ds/product/panel_title.mdx
Normal 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}]
|
||||
```
|
||||
25
frontend/src/app/main/ui/ds/product/panel_title.scss
Normal file
25
frontend/src/app/main/ui/ds/product/panel_title.scss
Normal 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);
|
||||
}
|
||||
21
frontend/src/app/main/ui/ds/product/panel_title.stories.jsx
Normal file
21
frontend/src/app/main/ui/ds/product/panel_title.stories.jsx
Normal 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 = {};
|
||||
@@ -10,6 +10,7 @@ $z-index-200: 200;
|
||||
$z-index-300: 300;
|
||||
$z-index-400: 400;
|
||||
$z-index-500: 500;
|
||||
$z-index-600: 600;
|
||||
|
||||
:global(:root) {
|
||||
--z-index-auto: #{$z-index-auto}; // Index for elements such as workspace, rulers ...
|
||||
@@ -18,4 +19,5 @@ $z-index-500: 500;
|
||||
--z-index-set: #{$z-index-300}; // Index for configuration elements like modals, color picker, grid edition elements
|
||||
--z-index-dropdown: #{$z-index-400}; // Index for dropdown like elements, selects, menus, dropdowns
|
||||
--z-index-notifications: #{$z-index-500}; // Index for notification
|
||||
--z-index-loaders: #{$z-index-600}; // Index for loaders
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
|
||||
touched? (and (contains? (:data @form) input-name)
|
||||
(get-in @form [:touched input-name]))
|
||||
|
||||
error (get-in @form [:errors input-name])
|
||||
|
||||
value (get-in @form [:data input-name] "")
|
||||
@@ -52,7 +53,8 @@
|
||||
(let [form (mf/use-ctx context)
|
||||
disabled? (or (and (some? form)
|
||||
(or (not (:valid @form))
|
||||
(seq (:external-errors @form))))
|
||||
(seq (:async-errors @form))
|
||||
(seq (:extra-errors @form))))
|
||||
(true? disabled))
|
||||
handle-key-down-save
|
||||
(mf/use-fn
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
[app.main.ui.releases.v2-10]
|
||||
[app.main.ui.releases.v2-11]
|
||||
[app.main.ui.releases.v2-12]
|
||||
[app.main.ui.releases.v2-13]
|
||||
[app.main.ui.releases.v2-2]
|
||||
[app.main.ui.releases.v2-3]
|
||||
[app.main.ui.releases.v2-4]
|
||||
@@ -103,4 +104,4 @@
|
||||
|
||||
(defmethod rc/render-release-notes "0.0"
|
||||
[params]
|
||||
(rc/render-release-notes (assoc params :version "2.12")))
|
||||
(rc/render-release-notes (assoc params :version "2.13")))
|
||||
|
||||
118
frontend/src/app/main/ui/releases/v2_13.cljs
Normal file
118
frontend/src/app/main/ui/releases/v2_13.cljs
Normal file
@@ -0,0 +1,118 @@
|
||||
;; 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.releases.v2-13
|
||||
(:require-macros [app.main.style :as stl])
|
||||
(:require
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.ui.releases.common :as c]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defmethod c/render-release-notes "2.13"
|
||||
[{:keys [slide klass next finish navigate version]}]
|
||||
(mf/html
|
||||
(case slide
|
||||
:start
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.13-slide-0.jpg"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Penpot 2.13 is here!"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"What’s new in Penpot?"]
|
||||
|
||||
[:div {:class (stl/css :version-tag)}
|
||||
(dm/str "Version " version)]]
|
||||
|
||||
[:div {:class (stl/css :features-block)}
|
||||
[:span {:class (stl/css :feature-title)}
|
||||
"The first release of the year, and we’re just getting started 🚀"]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This is our first release of the year, and it sets the tone for what’s coming next. We’re kicking off an exciting year where we’ll take Penpot to a whole new level, with improved performance, stronger design system foundations, long-requested features, and new capabilities that unlock better workflows for teams."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"This release brings two highlights the community has been asking for, along with solid improvements under the hood to keep everything fast and smooth."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Let’s dive in!"]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:button {:class (stl/css :next-btn)
|
||||
:on-click next} "Continue"]]]]]]
|
||||
|
||||
0
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.13-trash.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "The Trash"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"The Trash"]]
|
||||
|
||||
[:div {:class (stl/css :feature)}
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Deleting a file no longer means it’s gone forever. Introducing The Trash, a dedicated space in the dashboard where deleted files and projects live before being permanently removed."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"From here, you can recover content deleted by mistake or clean things up for good when you’re sure you don’t need them anymore. The Trash works for both files and projects, and items are automatically removed after a period of time depending on your Penpot plan."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Highly requested, long overdue, and now officially here."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 3}]
|
||||
|
||||
[:button {:on-click next
|
||||
:class (stl/css :next-btn)} "Continue"]]]]]]
|
||||
|
||||
1
|
||||
[:div {:class (stl/css-case :modal-overlay true)}
|
||||
[:div.animated {:class klass}
|
||||
[:div {:class (stl/css :modal-container)}
|
||||
[:img {:src "images/features/2.13-shadow-tokens.gif"
|
||||
:class (stl/css :start-image)
|
||||
:border "0"
|
||||
:alt "Shadow tokens: Reusable shadows, at last!"}]
|
||||
|
||||
[:div {:class (stl/css :modal-content)}
|
||||
[:div {:class (stl/css :modal-header)}
|
||||
[:h1 {:class (stl/css :modal-title)}
|
||||
"Shadow tokens: Reusable shadows, at last!"]]
|
||||
[:div {:class (stl/css :feature)}
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"With Shadow tokens, we’re introducing our second composite token, right after Typography tokens. This is a big step forward for design systems in Penpot."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Until now, shadows couldn’t be defined as reusable styles the way colors could before color tokens existed. Shadow tokens change that. You can now create reusable, consistent shadows, made of one or multiple layers, fully tokenized and ready to scale across your designs."]
|
||||
|
||||
[:p {:class (stl/css :feature-content)}
|
||||
"Each shadow can reference existing tokens or use custom values, supports both Drop Shadow and Inner Shadow, and even allows shadow tokens to reference other shadow tokens. A brand-new capability, unlocked."]]
|
||||
|
||||
[:div {:class (stl/css :navigation)}
|
||||
|
||||
[:& c/navigation-bullets
|
||||
{:slide slide
|
||||
:navigate navigate
|
||||
:total 2}]
|
||||
|
||||
[:button {:on-click finish
|
||||
:class (stl/css :next-btn)} "Let's go"]]]]]])))
|
||||
|
||||
102
frontend/src/app/main/ui/releases/v2_13.scss
Normal file
102
frontend/src/app/main/ui/releases/v2_13.scss
Normal file
@@ -0,0 +1,102 @@
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
//
|
||||
// Copyright (c) KALEIDOS INC
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
|
||||
.modal-overlay {
|
||||
@extend .modal-overlay-base;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
display: grid;
|
||||
grid-template-columns: deprecated.$s-324 1fr;
|
||||
height: deprecated.$s-500;
|
||||
width: deprecated.$s-888;
|
||||
border-radius: deprecated.$br-8;
|
||||
background-color: var(--modal-background-color);
|
||||
border: deprecated.$s-2 solid var(--modal-border-color);
|
||||
}
|
||||
|
||||
.start-image {
|
||||
width: deprecated.$s-324;
|
||||
border-radius: deprecated.$br-8 0 0 deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: deprecated.$s-40;
|
||||
display: grid;
|
||||
grid-template-rows: auto 1fr deprecated.$s-32;
|
||||
gap: deprecated.$s-24;
|
||||
|
||||
a {
|
||||
color: var(--button-primary-background-color-rest);
|
||||
}
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.version-tag {
|
||||
@include deprecated.flexCenter;
|
||||
@include deprecated.headlineSmallTypography;
|
||||
height: deprecated.$s-32;
|
||||
width: deprecated.$s-96;
|
||||
background-color: var(--communication-tag-background-color);
|
||||
color: var(--communication-tag-foreground-color);
|
||||
border-radius: deprecated.$br-8;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
@include deprecated.headlineLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.features-block {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-16;
|
||||
width: deprecated.$s-440;
|
||||
}
|
||||
|
||||
.feature {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.feature-title {
|
||||
@include deprecated.bodyLargeTypography;
|
||||
color: var(--modal-title-foreground-color);
|
||||
}
|
||||
|
||||
.feature-content {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
margin: 0;
|
||||
color: var(--modal-text-foreground-color);
|
||||
}
|
||||
|
||||
.feature-list {
|
||||
@include deprecated.bodyMediumTypography;
|
||||
color: var(--modal-text-foreground-color);
|
||||
list-style: disc;
|
||||
display: grid;
|
||||
gap: deprecated.$s-8;
|
||||
}
|
||||
|
||||
.navigation {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-areas: "bullets button";
|
||||
}
|
||||
|
||||
.next-btn {
|
||||
@extend .button-primary;
|
||||
width: deprecated.$s-100;
|
||||
justify-self: flex-end;
|
||||
grid-area: button;
|
||||
}
|
||||
@@ -308,6 +308,16 @@
|
||||
[:div {:class (stl/css :sign-info)}
|
||||
[:button {:on-click on-click} (tr "labels.retry")]]]))
|
||||
|
||||
(mf/defc webgl-context-lost*
|
||||
[]
|
||||
(let [on-reload (mf/use-fn #(js/location.reload))]
|
||||
[:> error-container* {}
|
||||
[:div {:class (stl/css :main-message)} (tr "errors.webgl-context-lost.main-message")]
|
||||
[:div {:class (stl/css :desc-message)} (tr "errors.webgl-context-lost.desc-message")]
|
||||
[:div {:class (stl/css :buttons-container)}
|
||||
[:> button* {:variant "primary" :on-click on-reload}
|
||||
(tr "labels.reload-page")]]]))
|
||||
|
||||
(defn- generate-report
|
||||
[data]
|
||||
(try
|
||||
@@ -437,6 +447,7 @@
|
||||
(rx/of default)
|
||||
(rx/throw cause)))))))
|
||||
|
||||
|
||||
(mf/defc exception-section*
|
||||
{::mf/private true}
|
||||
[{:keys [data route] :as props}]
|
||||
@@ -469,6 +480,9 @@
|
||||
:service-unavailable
|
||||
[:> service-unavailable*]
|
||||
|
||||
:webgl-context-lost
|
||||
[:> webgl-context-lost*]
|
||||
|
||||
[:> internal-error* props])))
|
||||
|
||||
(mf/defc context-wrapper*
|
||||
|
||||
@@ -218,6 +218,10 @@
|
||||
|
||||
design-tokens? (features/use-feature "design-tokens/v1")
|
||||
|
||||
wasm-renderer-enabled? (features/use-feature "render-wasm/v1")
|
||||
|
||||
first-frame-rendered? (mf/use-state false)
|
||||
|
||||
background-color (:background-color wglobal)]
|
||||
|
||||
(mf/with-effect []
|
||||
@@ -242,6 +246,17 @@
|
||||
(when (and file-loaded? (not page-id))
|
||||
(st/emit! (dcm/go-to-workspace :file-id file-id ::rt/replace true))))
|
||||
|
||||
(mf/with-effect [file-id page-id]
|
||||
(reset! first-frame-rendered? false))
|
||||
|
||||
(mf/with-effect []
|
||||
(let [handle-wasm-render
|
||||
(fn [_]
|
||||
(reset! first-frame-rendered? true))
|
||||
listener-key (events/listen globals/document "penpot:wasm:render" handle-wasm-render)]
|
||||
(fn []
|
||||
(events/unlistenByKey listener-key))))
|
||||
|
||||
[:> (mf/provider ctx/current-project-id) {:value project-id}
|
||||
[:> (mf/provider ctx/current-file-id) {:value file-id}
|
||||
[:> (mf/provider ctx/current-page-id) {:value page-id}
|
||||
@@ -250,15 +265,22 @@
|
||||
[:> modal-container*]
|
||||
[:section {:class (stl/css :workspace)
|
||||
:style {:background-color background-color
|
||||
:touch-action "none"}}
|
||||
:touch-action "none"
|
||||
:position "relative"}}
|
||||
[:> context-menu*]
|
||||
(if (and file-loaded? page-id)
|
||||
(when (and file-loaded? page-id)
|
||||
[:> workspace-inner*
|
||||
{:page-id page-id
|
||||
:file-id file-id
|
||||
:file file
|
||||
:wglobal wglobal
|
||||
:layout layout}]
|
||||
:layout layout}])
|
||||
(when (or (not (and file-loaded? page-id))
|
||||
;; in wasm renderer, extend the pixel loader until the first frame is rendered
|
||||
;; but do not apply it when switching pages
|
||||
(and wasm-renderer-enabled?
|
||||
(not file-loaded?)
|
||||
(not @first-frame-rendered?)))
|
||||
[:> workspace-loader*])]]]]]]))
|
||||
|
||||
(mf/defc workspace-page*
|
||||
|
||||
@@ -20,7 +20,13 @@
|
||||
}
|
||||
|
||||
.workspace-loader {
|
||||
grid-area: viewport;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: var(--z-index-loaders);
|
||||
background-color: var(--color-background-primary);
|
||||
}
|
||||
|
||||
.workspace-content {
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
[app.main.ui.comments :as cmt]
|
||||
[app.main.ui.components.dropdown :refer [dropdown]]
|
||||
[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.product.empty-state :refer [empty-state*]]
|
||||
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -121,15 +121,12 @@
|
||||
(st/emit! (with-meta (dcmt/open-thread thread) {::ev/origin "viewer"}))
|
||||
(st/emit! (dwcm/navigate-to-comment thread)))))]
|
||||
|
||||
[:div {:class (stl/css-case :comments-section true
|
||||
:from-viewer from-viewer)}
|
||||
[:div {:class (stl/css-case :comments-section-title true
|
||||
:viewer-title from-viewer)}
|
||||
[:span (tr "labels.comments")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click close-section
|
||||
:icon i/close}]]
|
||||
[:div {:class (stl/css-case :comments-section true
|
||||
:from-viewer from-viewer)}
|
||||
|
||||
[:> panel-title* {:class (stl/css :comments-title)
|
||||
:text (tr "labels.comments")
|
||||
:on-close close-section}]
|
||||
|
||||
[:button {:class (stl/css :mode-dropdown-wrapper)
|
||||
:on-click toggle-mode-selector}
|
||||
|
||||
@@ -18,25 +18,8 @@
|
||||
padding: 0 deprecated.$s-8;
|
||||
}
|
||||
|
||||
.comments-section-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);
|
||||
}
|
||||
}
|
||||
|
||||
.viewer-title {
|
||||
margin: 0;
|
||||
margin-block-start: deprecated.$s-8;
|
||||
.comments-title {
|
||||
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
|
||||
}
|
||||
|
||||
.mode-dropdown-wrapper {
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
[app.common.data.macros :as dm]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(mf/defc debug-panel*
|
||||
@@ -35,12 +34,9 @@
|
||||
(st/emit! (dw/remove-layout-flag :debug-panel))))]
|
||||
|
||||
[:div {:class (dm/str class " " (stl/css :debug-panel))}
|
||||
[:div {:class (stl/css :panel-title)}
|
||||
[:span "Debugging tools"]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:aria-label (tr "labels.close")
|
||||
:on-click handle-close
|
||||
:icon i/close}]]
|
||||
[:> panel-title* {:class (stl/css :debug-panel-title)
|
||||
:text (tr "workspace.debug.title")
|
||||
:on-close handle-close}]
|
||||
|
||||
[:div {:class (stl/css :debug-panel-inner)}
|
||||
(for [option (sort-by d/name dbg/options)]
|
||||
|
||||
@@ -12,21 +12,12 @@
|
||||
background-color: var(--panel-background-color);
|
||||
}
|
||||
|
||||
.panel-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);
|
||||
.debug-panel-title {
|
||||
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
|
||||
}
|
||||
|
||||
span {
|
||||
@include deprecated.flexCenter;
|
||||
flex-grow: 1;
|
||||
color: var(--title-foreground-color-hover);
|
||||
}
|
||||
.debug-panel-inner {
|
||||
padding: deprecated.$s-16 deprecated.$s-8;
|
||||
}
|
||||
|
||||
.checkbox-wrapper {
|
||||
@@ -39,7 +30,3 @@
|
||||
@extend .checkbox-icon;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-panel-inner {
|
||||
padding: deprecated.$s-16 deprecated.$s-8;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.refs :as refs]
|
||||
[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]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
@@ -125,11 +125,9 @@
|
||||
(map (d/getf objects)))]
|
||||
|
||||
[:div {:class (stl/css :shape-info)}
|
||||
[:div {:class (stl/css :shape-info-title)}
|
||||
[:span "Debug"]
|
||||
[:div {:class (stl/css :close-button)
|
||||
:on-click #(dbg/disable! :shape-panel)}
|
||||
deprecated-icon/close]]
|
||||
[:> panel-title* {:class (stl/css :shape-info-title)
|
||||
:text "Debug"
|
||||
:on-close #(dbg/disable! :shape-panel)}]
|
||||
|
||||
(if (empty? selected)
|
||||
[:div {:class (stl/css :attrs-container)} "No shapes selected"]
|
||||
|
||||
@@ -16,34 +16,7 @@
|
||||
}
|
||||
|
||||
.shape-info-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);
|
||||
}
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
|
||||
}
|
||||
|
||||
.attrs-container {
|
||||
|
||||
@@ -13,23 +13,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -55,7 +55,6 @@
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr
|
||||
(mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
|
||||
@@ -9,13 +9,17 @@
|
||||
(:require
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.types.token :as tk]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.data.workspace.shapes :as dwsh]
|
||||
[app.main.data.workspace.tokens.application :as dwta]
|
||||
[app.main.features :as features]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||
[app.main.ui.components.select :refer [select]]
|
||||
[app.main.ui.context :as muc]
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
@@ -39,11 +43,16 @@
|
||||
(defn- check-layer-menu-props
|
||||
[old-props new-props]
|
||||
(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")
|
||||
(unchecked-get new-props "class"))
|
||||
(identical? (unchecked-get old-props "ids")
|
||||
(unchecked-get new-props "ids"))
|
||||
(identical? old-applied-tokens
|
||||
new-applied-tokens)
|
||||
(identical? (get old-values :opacity)
|
||||
(get new-values :opacity))
|
||||
(identical? (get old-values :blend-mode)
|
||||
@@ -53,12 +62,53 @@
|
||||
(identical? (get old-values :hidden)
|
||||
(get new-values :hidden)))))
|
||||
|
||||
(mf/defc numeric-input-wrapper*
|
||||
{::mf/private true}
|
||||
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||
tokens (mf/with-memo [tokens name]
|
||||
(delay
|
||||
(-> (deref tokens)
|
||||
(select-keys (get tk/tokens-by-input name))
|
||||
(not-empty))))
|
||||
|
||||
on-detach-attr (mf/use-fn
|
||||
(mf/deps on-detach name)
|
||||
#(on-detach % name))
|
||||
|
||||
applied-token (get applied-tokens name)
|
||||
opacity-value (or (get values name) 1)
|
||||
|
||||
props (mf/spread-props props
|
||||
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||
(= :multiple opacity-value))
|
||||
(tr "settings.multiple")
|
||||
"--")
|
||||
:applied-token applied-token
|
||||
:tokens (if (delay? tokens) @tokens tokens)
|
||||
:align align
|
||||
:on-detach on-detach-attr
|
||||
:name name
|
||||
:value (* 100 opacity-value)})]
|
||||
[:> numeric-input* props]))
|
||||
|
||||
(mf/defc layer-menu*
|
||||
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
||||
[{:keys [ids values]}]
|
||||
(let [hidden? (get values :hidden)
|
||||
[{:keys [ids values applied-tokens]}]
|
||||
(let [token-numeric-inputs
|
||||
(features/use-feature "tokens/numeric-input")
|
||||
|
||||
hidden? (get values :hidden)
|
||||
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-opacity (opacity->string (:opacity values))
|
||||
|
||||
@@ -118,6 +168,17 @@
|
||||
(let [value (/ value 100)]
|
||||
(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
|
||||
(mf/use-fn
|
||||
(mf/deps ids)
|
||||
@@ -176,8 +237,9 @@
|
||||
preview-complete?))
|
||||
(swap! state* assoc :selected-blend-mode current-blend-mode)))
|
||||
|
||||
[:div {:class (stl/css-case :element-set-content true
|
||||
:hidden hidden?)}
|
||||
[:section {:class (stl/css-case :element-set-content true
|
||||
:hidden hidden?)
|
||||
:aria-label "layer-menu-section"}
|
||||
[:div {:class (stl/css :select)}
|
||||
[:& select
|
||||
{:default-value selected-blend-mode
|
||||
@@ -187,16 +249,34 @@
|
||||
:class (stl/css-case :hidden-select hidden?)
|
||||
:on-pointer-enter-option handle-blend-mode-enter
|
||||
: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*
|
||||
{:value current-opacity
|
||||
:placeholder "--"
|
||||
:on-change handle-opacity-change
|
||||
:min 0
|
||||
:max 100
|
||||
:className (stl/css :numeric-input)}]]
|
||||
|
||||
|
||||
|
||||
(if token-numeric-inputs
|
||||
|
||||
[:> numeric-input-wrapper*
|
||||
{:on-change on-opacity-change
|
||||
:on-detach on-detach-token
|
||||
:icon i/percentage
|
||||
:min 0
|
||||
:max 100
|
||||
:name :opacity
|
||||
: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)}
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
|
||||
@use "refactor/common-refactor.scss" as deprecated;
|
||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||
@use "ds/_utils.scss" as *;
|
||||
|
||||
.element-set-content {
|
||||
@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)};
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:type type
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
|
||||
[:> measures-menu* {:ids ids
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:type type
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
|
||||
[:> measures-menu* {:ids ids
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:type shape-type
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
[:> measures-menu* {:ids ids
|
||||
:applied-tokens applied-tokens
|
||||
|
||||
@@ -111,6 +111,7 @@
|
||||
[:div {:class (stl/css :options)}
|
||||
[:> layer-menu* {:type type
|
||||
:ids layer-ids
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
[:> measures-menu* {:type type
|
||||
:ids measure-ids
|
||||
|
||||
@@ -382,7 +382,7 @@
|
||||
objects
|
||||
objects)))
|
||||
|
||||
[layer-ids layer-values]
|
||||
[layer-ids layer-values layer-tokens]
|
||||
(get-attrs shapes objects :layer)
|
||||
|
||||
[text-ids text-values]
|
||||
@@ -406,7 +406,7 @@
|
||||
[exports-ids exports-values]
|
||||
(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)
|
||||
|
||||
[layout-item-ids layout-item-values {}]
|
||||
@@ -442,6 +442,7 @@
|
||||
(when-not (empty? layer-ids)
|
||||
[:> layer-menu* {:type type
|
||||
:ids layer-ids
|
||||
:applied-tokens layer-tokens
|
||||
:values layer-values}])
|
||||
|
||||
(when-not (empty? measure-ids)
|
||||
@@ -459,7 +460,7 @@
|
||||
{:type type
|
||||
:ids layout-container-ids
|
||||
:values layout-container-values
|
||||
:applied-tokens layout-contianer-tokens
|
||||
:applied-tokens layout-container-tokens
|
||||
:multiple true}]
|
||||
|
||||
(when (or is-layout-child? has-flex-layout-container?)
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:applied-tokens applied-tokens
|
||||
:type type
|
||||
:values layer-values}]
|
||||
[:> measures-menu* {:ids ids
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:type type
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
[:> measures-menu* {:ids ids
|
||||
:type type
|
||||
|
||||
@@ -125,6 +125,7 @@
|
||||
[:*
|
||||
[:> layer-menu* {:ids ids
|
||||
:type type
|
||||
:applied-tokens applied-tokens
|
||||
:values layer-values}]
|
||||
[:> measures-menu*
|
||||
{:ids ids
|
||||
|
||||
@@ -18,8 +18,8 @@
|
||||
[app.main.data.workspace.shortcuts]
|
||||
[app.main.store :as st]
|
||||
[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.product.panel-title :refer [panel-title*]]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :refer [tr]]
|
||||
[app.util.strings :refer [matches-search]]
|
||||
@@ -487,13 +487,9 @@
|
||||
(dom/focus! (dom/get-element "shortcut-search")))
|
||||
|
||||
[:div {:class (dm/str class " " (stl/css :shortcuts))}
|
||||
[:div {:class (stl/css :shortcuts-header)}
|
||||
[:div {:class (stl/css :shortcuts-title)} (tr "shortcuts.title")]
|
||||
[:> icon-button* {:variant "ghost"
|
||||
:icon i/close
|
||||
:class (stl/css :shortcuts-close-button)
|
||||
:on-click close-fn
|
||||
:aria-label (tr "labels.close")}]]
|
||||
[:> panel-title* {:class (stl/css :shortcuts-title)
|
||||
:text (tr "shortcuts.title")
|
||||
:on-close close-fn}]
|
||||
|
||||
[:div {:class (stl/css :search-field)}
|
||||
[:> search-bar* {:on-change on-search-term-change-2
|
||||
|
||||
@@ -18,27 +18,8 @@
|
||||
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12;
|
||||
}
|
||||
|
||||
.shortcuts-header {
|
||||
@include deprecated.flexCenter;
|
||||
@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;
|
||||
}
|
||||
.shortcuts-title {
|
||||
margin: var(--sp-s) var(--sp-s) 0 var(--sp-s);
|
||||
}
|
||||
|
||||
.section {
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[app.main.data.helpers :as dsh]
|
||||
[app.main.data.modal :as modal]
|
||||
[app.main.data.workspace :as dw]
|
||||
[app.main.features :as features]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.components.title-bar :refer [title-bar*]]
|
||||
@@ -22,9 +23,11 @@
|
||||
[app.main.ui.hooks :as hooks]
|
||||
[app.main.ui.icons :as deprecated-icon]
|
||||
[app.main.ui.notifications.badge :refer [badge-notification]]
|
||||
[app.render-wasm.api :as wasm.api]
|
||||
[app.util.dom :as dom]
|
||||
[app.util.i18n :as i18n :refer [tr]]
|
||||
[app.util.keyboard :as kbd]
|
||||
[app.util.timers :as timers]
|
||||
[cuerdas.core :as str]
|
||||
[okulary.core :as l]
|
||||
[rumext.v2 :as mf]))
|
||||
@@ -52,6 +55,8 @@
|
||||
refs/workspace-data
|
||||
=))
|
||||
|
||||
|
||||
|
||||
;; --- 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)))
|
||||
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
|
||||
(mf/use-fn
|
||||
(mf/deps id)
|
||||
@@ -155,7 +176,7 @@
|
||||
:selected selected?)
|
||||
:data-testid (dm/str "page-" id)
|
||||
:tab-index "0"
|
||||
:on-click navigate-fn
|
||||
:on-click on-click
|
||||
:on-double-click on-double-click
|
||||
:on-context-menu on-context-menu}
|
||||
[:div {:class (stl/css :page-icon)}
|
||||
|
||||
@@ -273,4 +273,4 @@
|
||||
{:label (tr "workspace.tokens.import-menu-folder-option") :value :folder}]
|
||||
:on-click handle-import-action
|
||||
:text-render render-button-text
|
||||
:default :zip}]]]))
|
||||
:default :file}]]]))
|
||||
|
||||
@@ -14,8 +14,10 @@
|
||||
[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.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.i18n :refer [tr]]
|
||||
[cuerdas.core :as str]
|
||||
[rumext.v2 :as mf]))
|
||||
|
||||
(defn- get-sorted-token-groups
|
||||
@@ -120,7 +122,27 @@
|
||||
|
||||
[empty-group filled-group]
|
||||
(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]
|
||||
(when (and tokens-lib
|
||||
@@ -134,6 +156,7 @@
|
||||
|
||||
[:*
|
||||
[:& token-context-menu]
|
||||
[:> token-node-context-menu* {:on-delete-node delete-node}]
|
||||
|
||||
[:& selected-set-info* {:tokens-lib tokens-lib
|
||||
:selected-token-set-id selected-token-set-id}]
|
||||
|
||||
@@ -140,6 +140,9 @@
|
||||
error
|
||||
(get-in @form [:errors input-name])
|
||||
|
||||
extra-error
|
||||
(get-in @form [:extra-errors input-name])
|
||||
|
||||
value
|
||||
(get-in @form [:data input-name] "")
|
||||
|
||||
@@ -247,9 +250,14 @@
|
||||
:hint-type (:type hint)})
|
||||
|
||||
props
|
||||
(if (and error touched?)
|
||||
(cond
|
||||
(and error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
(and extra-error touched?)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message extra-error)})
|
||||
:else
|
||||
props)]
|
||||
|
||||
(mf/with-effect [resolve-stream tokens token input-name]
|
||||
|
||||
@@ -236,12 +236,14 @@
|
||||
(on-composite-input-change form field value false))
|
||||
([form field value trim?]
|
||||
(letfn [(clean-errors [errors]
|
||||
(-> errors
|
||||
(dissoc field)
|
||||
(not-empty)))]
|
||||
(some-> errors
|
||||
(update :value #(when (map? %) (dissoc % field)))
|
||||
(update :value #(when (seq %) %))
|
||||
(not-empty)))]
|
||||
(swap! form (fn [state]
|
||||
(-> state
|
||||
(assoc-in [:data :value field] (if trim? (str/trim value) value))
|
||||
(assoc-in [:touched :value field] true)
|
||||
(update :errors clean-errors)
|
||||
(update :extra-errors clean-errors)))))))
|
||||
|
||||
@@ -257,6 +259,9 @@
|
||||
value
|
||||
(get-in @form [:data :value input-name] "")
|
||||
|
||||
touched?
|
||||
(get-in @form [:touched :value input-name])
|
||||
|
||||
resolve-stream
|
||||
(mf/with-memo [token]
|
||||
(if-let [value (get-in token [:value input-name])]
|
||||
@@ -284,7 +289,7 @@
|
||||
:hint-message (:message hint)
|
||||
:hint-type (:type hint)})
|
||||
props
|
||||
(if error
|
||||
(if (and touched? error)
|
||||
(mf/spread-props props {:hint-type "error"
|
||||
:hint-message (:message error)})
|
||||
props)
|
||||
@@ -332,6 +337,7 @@
|
||||
message (tr "workspace.tokens.resolved-value" (or resolved-value value))]
|
||||
(swap! form update :errors dissoc :value)
|
||||
(swap! form update :extra-errors dissoc :value)
|
||||
(swap! form update :async-errors dissoc :reference)
|
||||
(if (= input-value (str resolved-value))
|
||||
(reset! hint* {})
|
||||
(reset! hint* {:message message :type "hint"})))))))]
|
||||
|
||||
@@ -23,21 +23,19 @@
|
||||
(let [token-type
|
||||
(or (:type token) token-type)
|
||||
|
||||
tokens-in-selected-set
|
||||
(mf/deref refs/workspace-all-tokens-in-selected-set)
|
||||
|
||||
token-path
|
||||
(mf/with-memo [token]
|
||||
(cft/token-name->path (:name token)))
|
||||
|
||||
tokens-tree-in-selected-set
|
||||
(mf/with-memo [token-path tokens-in-selected-set]
|
||||
(-> (ctob/tokens-tree tokens-in-selected-set)
|
||||
all-tokens (mf/deref refs/workspace-all-tokens-map)
|
||||
|
||||
all-tokens
|
||||
(mf/with-memo [token-path all-tokens]
|
||||
(-> (ctob/tokens-tree all-tokens)
|
||||
(d/dissoc-in token-path)))
|
||||
props
|
||||
(mf/spread-props props {:token-type token-type
|
||||
:tokens-tree-in-selected-set tokens-tree-in-selected-set
|
||||
:tokens-in-selected-set tokens-in-selected-set
|
||||
:all-token-tree all-tokens
|
||||
:token token})
|
||||
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")})
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
[app.main.data.helpers :as dh]
|
||||
[app.main.data.modal :as modal]
|
||||
[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.propagation :as dwtp]
|
||||
[app.main.data.workspace.tokens.remapping :as remap]
|
||||
@@ -88,14 +89,13 @@
|
||||
action
|
||||
is-create
|
||||
selected-token-set-id
|
||||
tokens-tree-in-selected-set
|
||||
all-token-tree
|
||||
token-type
|
||||
make-schema
|
||||
input-component
|
||||
initial
|
||||
type
|
||||
value-subfield
|
||||
tokens-in-selected-set
|
||||
input-value-placeholder] :as props}]
|
||||
|
||||
(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 (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
|
||||
(mf/with-memo [token]
|
||||
(or token {:type token-type}))
|
||||
@@ -124,6 +117,9 @@
|
||||
tokens
|
||||
(mf/deref refs/workspace-active-theme-sets-tokens)
|
||||
|
||||
tokens-in-selected-set
|
||||
(mf/deref refs/workspace-all-tokens-in-selected-set)
|
||||
|
||||
tokens
|
||||
(mf/with-memo [tokens tokens-in-selected-set token]
|
||||
;; Ensure that the resolved value uses the currently editing token
|
||||
@@ -134,8 +130,8 @@
|
||||
(assoc (:name token) token)))
|
||||
|
||||
schema
|
||||
(mf/with-memo [tokens-tree-in-selected-set active-tab]
|
||||
(make-schema tokens-tree-in-selected-set active-tab))
|
||||
(mf/with-memo [all-token-tree active-tab]
|
||||
(make-schema all-token-tree active-tab))
|
||||
|
||||
initial
|
||||
(mf/with-memo [token]
|
||||
@@ -148,6 +144,17 @@
|
||||
(fm/use-form :schema schema
|
||||
: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
|
||||
(mf/use-fn
|
||||
(fn [e]
|
||||
@@ -224,7 +231,12 @@
|
||||
:description description}))
|
||||
(dwtl/toggle-token-path path)
|
||||
(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)
|
||||
:form form
|
||||
|
||||
@@ -291,6 +291,7 @@
|
||||
[:color {:optional true} [:maybe :string]]
|
||||
[:color-result {:optional true} ::sm/any]
|
||||
[:inset {:optional true} [:maybe :boolean]]]]]
|
||||
|
||||
(if (= active-tab :reference)
|
||||
[:reference {:optional false} ::sm/text]
|
||||
[:reference {:optional true} [:maybe :string]])]]
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
|
||||
expandable? (d/nilv (seq tokens) false)
|
||||
|
||||
on-context-menu
|
||||
on-pill-context-menu
|
||||
(mf/use-fn
|
||||
(fn [event token]
|
||||
(dom/prevent-default event)
|
||||
@@ -98,6 +98,15 @@
|
||||
:errors (:errors 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
|
||||
(mf/use-fn
|
||||
(mf/deps type expandable?)
|
||||
@@ -159,4 +168,5 @@
|
||||
:selected-token-set-id selected-token-set-id
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
: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}])]))
|
||||
|
||||
@@ -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)))))
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@
|
||||
[app.common.path-names :as cpn]
|
||||
[app.common.types.tokens-lib :as ctob]
|
||||
[app.main.data.workspace.tokens.library-edit :as dwtl]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.store :as st]
|
||||
[app.main.ui.ds.layers.layer-button :refer [layer-button*]]
|
||||
[app.main.ui.workspace.tokens.management.token-pill :refer [token-pill*]]
|
||||
@@ -26,7 +27,8 @@
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[: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/schema schema:folder-node}
|
||||
@@ -39,22 +41,29 @@
|
||||
selected-token-set-id
|
||||
tokens-lib
|
||||
on-token-pill-click
|
||||
on-context-menu]}]
|
||||
on-pill-context-menu
|
||||
on-node-context-menu]}]
|
||||
(let [full-path (str (name type) "." (:path node))
|
||||
is-folder-expanded (contains? (set (or unfolded-token-paths [])) full-path)
|
||||
|
||||
swap-folder-expanded (mf/use-fn
|
||||
(mf/deps (:path node) type)
|
||||
(fn []
|
||||
(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)}
|
||||
[:> layer-button* {:label (:name node)
|
||||
:expanded is-folder-expanded
|
||||
:aria-expanded is-folder-expanded
|
||||
:aria-controls (str "folder-children-" (:path 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
|
||||
(let [children-fn (:children-fn node)]
|
||||
[:div {:class (stl/css :folder-children-wrapper)
|
||||
@@ -63,16 +72,17 @@
|
||||
(let [children (children-fn)]
|
||||
(for [child children]
|
||||
(if (not (:leaf child))
|
||||
[:ul {:class (stl/css :node-parent)}
|
||||
[:> folder-node* {:key (:path child)
|
||||
:type type
|
||||
[:ul {:class (stl/css :node-parent)
|
||||
:key (:path child)}
|
||||
[:> folder-node* {:type type
|
||||
:node child
|
||||
:unfolded-token-paths unfolded-token-paths
|
||||
:selected-shapes selected-shapes
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
: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
|
||||
:selected-token-set-id selected-token-set-id}]]
|
||||
(let [id (:id (:leaf child))
|
||||
@@ -84,7 +94,7 @@
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])))))]))]))
|
||||
:on-context-menu on-pill-context-menu}])))))]))]))
|
||||
|
||||
(def ^:private schema:token-tree
|
||||
[:map
|
||||
@@ -97,7 +107,8 @@
|
||||
[:selected-token-set-id {:optional true} :any]
|
||||
[:tokens-lib {:optional true} :any]
|
||||
[: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/schema schema:token-tree}
|
||||
@@ -110,12 +121,19 @@
|
||||
tokens-lib
|
||||
selected-token-set-id
|
||||
on-token-pill-click
|
||||
on-context-menu]}]
|
||||
on-pill-context-menu
|
||||
on-node-context-menu]}]
|
||||
(let [separator "."
|
||||
tree (mf/use-memo
|
||||
(mf/deps tokens)
|
||||
(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)}
|
||||
(for [node tree]
|
||||
(if (:leaf node)
|
||||
@@ -127,7 +145,7 @@
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
:on-click on-token-pill-click
|
||||
:on-context-menu on-context-menu}])
|
||||
:on-context-menu on-pill-context-menu}])
|
||||
;; Render segment folder
|
||||
[:ul {:class (stl/css :node-parent)
|
||||
:key (:path node)}
|
||||
@@ -138,6 +156,7 @@
|
||||
:is-selected-inside-layout is-selected-inside-layout
|
||||
:active-theme-tokens active-theme-tokens
|
||||
: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
|
||||
:selected-token-set-id selected-token-set-id}]]))]))
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||
[app.main.ui.ds.controls.combobox :refer [combobox*]]
|
||||
[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.foundations.assets.icon :refer [icon*] :as i]
|
||||
[app.main.ui.ds.foundations.typography.heading :refer [heading*]]
|
||||
@@ -85,21 +85,6 @@
|
||||
:on-click create-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
|
||||
[{:keys [change-view]}]
|
||||
(let [active-theme-paths (mf/deref refs/workspace-active-theme-paths)
|
||||
@@ -137,6 +122,9 @@
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(st/emit! (dwtl/delete-token-theme id)))
|
||||
on-switch-theme
|
||||
(fn []
|
||||
(st/emit! (dwtl/toggle-token-theme-active id)))
|
||||
on-edit-theme
|
||||
(fn [e]
|
||||
(dom/prevent-default e)
|
||||
@@ -146,16 +134,10 @@
|
||||
:class (stl/css :theme-row)}
|
||||
[:div {:class (stl/css :theme-switch-row)}
|
||||
|
||||
;; FIXME: FIREEEEEEEEEE THIS
|
||||
[:div {:on-click (fn [e]
|
||||
(dom/prevent-default e)
|
||||
(dom/stop-propagation e)
|
||||
(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]]
|
||||
[:> switch* {:id name
|
||||
:label name
|
||||
:on-change on-switch-theme
|
||||
:default-checked selected?}]]
|
||||
|
||||
|
||||
[:div {:class (stl/css :theme-actions-row)}
|
||||
|
||||
@@ -228,7 +228,7 @@
|
||||
:class (stl/css :main-toolbar-options-button)
|
||||
:icon i/bug
|
||||
:aria-pressed (contains? layout :debug-panel)
|
||||
:aria-label "Debugging tool"
|
||||
:aria-label (tr "workspace.toolbar.debug")
|
||||
:tooltip-placement "bottom"
|
||||
:on-click toggle-debug-panel}]])]]
|
||||
|
||||
|
||||
@@ -312,6 +312,11 @@
|
||||
(js/console.error "Error initializing canvas context:" e)
|
||||
false))]
|
||||
(reset! canvas-init? init?)
|
||||
(when init?
|
||||
;; Restore previous canvas pixels immediately after context initialization
|
||||
;; This happens before initialize-viewport is called
|
||||
(wasm.api/apply-canvas-blur)
|
||||
(wasm.api/restore-previous-canvas-pixels))
|
||||
(when-not init?
|
||||
(js/alert "WebGL not supported")
|
||||
(st/emit! (dcm/go-to-dashboard-recent))))))))
|
||||
@@ -340,6 +345,7 @@
|
||||
|
||||
(mf/with-effect [@canvas-init? zoom vbox background]
|
||||
(when (and @canvas-init? (not @initialized?))
|
||||
(wasm.api/clear-canvas-pixels)
|
||||
(wasm.api/initialize-viewport base-objects zoom vbox background)
|
||||
(reset! initialized? true)))
|
||||
|
||||
|
||||
@@ -1185,7 +1185,6 @@
|
||||
{:cmd :export-shapes
|
||||
:profile-id (:profile-id @st/state)
|
||||
:wait true
|
||||
:skip-children (:skip-children value false)
|
||||
:exports [{:file-id file-id
|
||||
:page-id page-id
|
||||
:object-id id
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
["react-dom/server" :as rds]
|
||||
[app.common.data :as d]
|
||||
[app.common.data.macros :as dm]
|
||||
[app.common.exceptions :as ex]
|
||||
[app.common.files.helpers :as cfh]
|
||||
[app.common.logging :as log]
|
||||
[app.common.math :as mth]
|
||||
@@ -21,7 +22,6 @@
|
||||
[app.common.types.text :as txt]
|
||||
[app.common.uuid :as uuid]
|
||||
[app.config :as cf]
|
||||
[app.main.data.render-wasm :as drw]
|
||||
[app.main.refs :as refs]
|
||||
[app.main.render :as render]
|
||||
[app.main.store :as st]
|
||||
@@ -29,6 +29,7 @@
|
||||
[app.main.worker :as mw]
|
||||
[app.render-wasm.api.fonts :as f]
|
||||
[app.render-wasm.api.texts :as t]
|
||||
[app.render-wasm.api.webgl :as webgl]
|
||||
[app.render-wasm.deserializers :as dr]
|
||||
[app.render-wasm.helpers :as h]
|
||||
[app.render-wasm.mem :as mem]
|
||||
@@ -37,7 +38,6 @@
|
||||
[app.render-wasm.serializers :as sr]
|
||||
[app.render-wasm.serializers.color :as sr-clr]
|
||||
[app.render-wasm.svg-filters :as svg-filters]
|
||||
;; FIXME: rename; confunsing name
|
||||
[app.render-wasm.wasm :as wasm]
|
||||
[app.util.debug :as dbg]
|
||||
[app.util.dom :as dom]
|
||||
@@ -279,30 +279,6 @@
|
||||
[string]
|
||||
(+ (count string) 1))
|
||||
|
||||
(defn- create-webgl-texture-from-image
|
||||
"Creates a WebGL texture from an HTMLImageElement or ImageBitmap and returns the texture object"
|
||||
[gl image-element]
|
||||
(let [texture (.createTexture ^js gl)]
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) texture)
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_S ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_WRAP_T ^js gl) (.-CLAMP_TO_EDGE ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MIN_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texParameteri ^js gl (.-TEXTURE_2D ^js gl) (.-TEXTURE_MAG_FILTER ^js gl) (.-LINEAR ^js gl))
|
||||
(.texImage2D ^js gl (.-TEXTURE_2D ^js gl) 0 (.-RGBA ^js gl) (.-RGBA ^js gl) (.-UNSIGNED_BYTE ^js gl) image-element)
|
||||
(.bindTexture ^js gl (.-TEXTURE_2D ^js gl) nil)
|
||||
texture))
|
||||
|
||||
(defn- get-webgl-context
|
||||
"Gets the WebGL context from the WASM module"
|
||||
[]
|
||||
(when wasm/context-initialized?
|
||||
(let [gl-obj (unchecked-get wasm/internal-module "GL")]
|
||||
(when gl-obj
|
||||
;; Get the current WebGL context from Emscripten
|
||||
;; The GL object has a currentContext property that contains the context handle
|
||||
(let [current-ctx (.-currentContext ^js gl-obj)]
|
||||
(when current-ctx
|
||||
(.-GLctx ^js current-ctx)))))))
|
||||
|
||||
(defn- get-texture-id-for-gl-object
|
||||
"Registers a WebGL texture with Emscripten's GL object system and returns its ID"
|
||||
@@ -332,8 +308,8 @@
|
||||
(->> (retrieve-image url)
|
||||
(rx/map
|
||||
(fn [img]
|
||||
(when-let [gl (get-webgl-context)]
|
||||
(let [texture (create-webgl-texture-from-image gl img)
|
||||
(when-let [gl (webgl/get-webgl-context)]
|
||||
(let [texture (webgl/create-webgl-texture-from-image gl img)
|
||||
texture-id (get-texture-id-for-gl-object texture)
|
||||
width (.-width ^js img)
|
||||
height (.-height ^js img)
|
||||
@@ -979,6 +955,7 @@
|
||||
(set-shape-grow-type grow-type))
|
||||
|
||||
(set-shape-layout shape)
|
||||
(set-layout-data shape)
|
||||
(set-shape-selrect selrect)
|
||||
|
||||
(let [pending_thumbnails (into [] (concat
|
||||
@@ -1055,8 +1032,9 @@
|
||||
(perf/end-measure "set-objects")
|
||||
(process-pending shapes thumbnails full noop-fn
|
||||
(fn []
|
||||
(when render-callback (render-callback))
|
||||
(render-finish)
|
||||
(if render-callback
|
||||
(render-callback)
|
||||
(render-finish))
|
||||
(ug/dispatch! (ug/event "penpot:wasm:set-objects")))))))
|
||||
|
||||
(defn clear-focus-mode
|
||||
@@ -1236,7 +1214,8 @@
|
||||
(dom/prevent-default event)
|
||||
(reset! wasm/context-lost? true)
|
||||
(log/warn :hint "WebGL context lost")
|
||||
(st/emit! (drw/context-lost)))
|
||||
(ex/raise :type :webgl-context-lost
|
||||
:hint "WebGL context lost"))
|
||||
|
||||
(defn init-canvas-context
|
||||
[canvas]
|
||||
@@ -1383,8 +1362,9 @@
|
||||
all-children
|
||||
(->> ids
|
||||
(mapcat #(cfh/get-children-with-self objects %)))]
|
||||
|
||||
(h/call wasm/internal-module "_init_shapes_pool" (count all-children))
|
||||
(run! (partial set-object objects) all-children)
|
||||
(run! set-object all-children)
|
||||
|
||||
(let [content (-> (calculate-bool* bool-type ids)
|
||||
(path.impl/path-data))]
|
||||
@@ -1447,6 +1427,12 @@
|
||||
|
||||
result)))
|
||||
|
||||
(defn apply-canvas-blur
|
||||
[]
|
||||
(when wasm/canvas
|
||||
(dom/set-style! wasm/canvas "filter" "blur(4px)")))
|
||||
|
||||
|
||||
(defn init-wasm-module
|
||||
[module]
|
||||
(let [default-fn (unchecked-get module "default")
|
||||
@@ -1468,3 +1454,8 @@
|
||||
(js/console.error cause)
|
||||
(p/resolved false)))))
|
||||
(p/resolved false))))
|
||||
|
||||
;; Re-export public WebGL functions
|
||||
(def capture-canvas-pixels webgl/capture-canvas-pixels)
|
||||
(def restore-previous-canvas-pixels webgl/restore-previous-canvas-pixels)
|
||||
(def clear-canvas-pixels webgl/clear-canvas-pixels)
|
||||
|
||||
166
frontend/src/app/render_wasm/api/webgl.cljs
Normal file
166
frontend/src/app/render_wasm/api/webgl.cljs
Normal 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))))
|
||||
@@ -12,6 +12,8 @@
|
||||
|
||||
;; Reference to the HTML canvas element.
|
||||
(defonce canvas nil)
|
||||
;; Reference to the captured pixels of the canvas (for page switching effect)
|
||||
(defonce canvas-pixels nil)
|
||||
|
||||
;; Reference to the Emscripten GL context wrapper.
|
||||
(defonce gl-context-handle nil)
|
||||
@@ -56,3 +58,4 @@
|
||||
:stroke-linecap shared/RawStrokeLineCap
|
||||
:stroke-linejoin shared/RawStrokeLineJoin
|
||||
:fill-rule shared/RawFillRule})
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
|
||||
(defn- load
|
||||
[locale]
|
||||
(let [path (str "./translation." locale ".js?version=" (:full cf/version))]
|
||||
(let [path (str "./translation." locale ".js?version=" cf/version-tag)]
|
||||
(->> (mod/import path)
|
||||
(p/fmap (fn [result] (unchecked-get result "default")))
|
||||
(p/fnly (fn [data cause]
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user