Compare commits

..

26 Commits

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

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

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

Fixes #8110

---
This is a Gittensor contribution.
gittensor:user:GlobalStar117
2026-01-20 15:51:25 +01:00
Alonso Torres
47775a9e2c Merge pull request #8134 from penpot/alotor-fix-plugins-export
🐛 Fix problem with export in plugins
2026-01-20 15:03:04 +01:00
Andrey Antukh
8191d04114 Use non-legacy config example on docker compose file 2026-01-20 13:25:55 +01:00
Alejandro Alonso
b7c2d9a079 Merge pull request #8130 from penpot/superalex-improve-zoom-pan-performance-7
🐛 Fix some tiles disappear after fast zoom and pan
2026-01-20 12:56:02 +01:00
Alejandro Alonso
aeb34a6f64 Merge pull request #8109 from penpot/superalex-fix-text-selrect-calculation
🐛 Render wasm typography token issues
2026-01-20 12:54:45 +01:00
Alejandro Alonso
6fa0c3af0c 🐛 Fix some tiles disappear after fast zoom and pan 2026-01-20 12:40:01 +01:00
Alejandro Alonso
260b9fb040 🐛 Fix texts with auto size updated via tokens with render wasm
activated
2026-01-20 12:39:17 +01:00
Alejandro Alonso
884954f4ff 🐛 Fix text selrect calculation 2026-01-20 12:37:57 +01:00
Andrey Antukh
88f0f75174 Merge pull request #8129 from penpot/niwinz-staging-bugfix-1
 Several improvements for build process
2026-01-20 12:26:55 +01:00
Andrey Antukh
1ffa956251 Include timestamp on version tag 2026-01-20 12:26:39 +01:00
Andrey Antukh
31054099ff Use pseudo-names on release builds of frontend (#8105) 2026-01-20 12:26:39 +01:00
Eva Marco
983487d73c 🐛 Fix shadow token reference validation (#8128) 2026-01-20 10:56:27 +01:00
Andrey Antukh
5c71c57dd9 Merge tag '2.12.1' 2025-12-30 15:37:30 +01:00
Andrey Antukh
5abc1aafb4 Merge tag '2.12.0-RC3' 2025-12-12 12:19:29 +01:00
Andrey Antukh
935728aa39 🔧 Backport build-tag github workflow from develop 2025-12-05 10:26:01 +01:00
55 changed files with 510 additions and 783 deletions

View File

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

View File

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

View File

@@ -24,6 +24,7 @@
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955) - Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012) - Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
- Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051) - Fix incorrect default option on tokens import dialog [Github #8051](https://github.com/penpot/penpot/pull/8051)
- Fix unhandled exception tokens creation dialog [Github #8110](https://github.com/penpot/penpot/issues/8110)
## 2.13.0 (Unreleased) ## 2.13.0 (Unreleased)

View File

@@ -474,8 +474,8 @@
:height #{:sizing :dimensions} :height #{:sizing :dimensions}
:max-width #{:sizing :dimensions} :max-width #{:sizing :dimensions}
:max-height #{:sizing :dimensions} :max-height #{:sizing :dimensions}
:x #{:dimensions} :x #{:spacing :dimensions}
:y #{:dimensions} :y #{:spacing :dimensions}
:rotation #{:number :rotation} :rotation #{:number :rotation}
:border-radius #{:border-radius :dimensions} :border-radius #{:border-radius :dimensions}
:row-gap #{:spacing :dimensions} :row-gap #{:spacing :dimensions}
@@ -488,7 +488,6 @@
:sided-margins #{:spacing :dimensions} :sided-margins #{:spacing :dimensions}
:line-height #{:line-height :number} :line-height #{:line-height :number}
:opacity #{:opacity} :opacity #{:opacity}
:stroke-width #{:stroke-width}
:font-size #{:font-size} :font-size #{:font-size}
:letter-spacing #{:letter-spacing} :letter-spacing #{:letter-spacing}
:fill #{:color} :fill #{:color}

View File

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

View File

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

View File

@@ -149,14 +149,12 @@ test.describe("Tokens: Apply token", () => {
await detachButton.click(); await detachButton.click();
// Open dropdown from input // Open dropdown from input
const dropdownBtn = layerMenuSection.getByLabel("Open token list"); const dropdownBtn = layerMenuSection.getByLabel('Open token list');
await expect(dropdownBtn).toBeVisible(); await expect(dropdownBtn).toBeVisible();
await dropdownBtn.click(); await dropdownBtn.click();
// Change token from dropdown // Change token from dropdown
const opacityLowOption = layerMenuSection.getByRole("option", { const opacityLowOption = layerMenuSection.getByRole('option', { name: 'opacity.low' });
name: "opacity.low",
});
await expect(opacityLowOption).toBeVisible(); await expect(opacityLowOption).toBeVisible();
await opacityLowOption.click(); await opacityLowOption.click();
@@ -484,219 +482,4 @@ test.describe("Tokens: Apply token", () => {
await expect(shadowSection).toHaveCount(2); await expect(shadowSection).toHaveCount(2);
}); });
}); });
test("User applies dimension token to a shape on width and height", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
// Apply token to width and height token from token panel
await tokensSidebar.getByRole("button", { name: "dimension.sm" }).click();
// Check if measures sections is visible on right sidebar
const measuresSection = page.getByRole("region", {
name: "shape-measures-section",
});
await expect(measuresSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const dimensionSMTokenPill = measuresSection.getByRole("button", {
name: "dimension.sm",
});
await expect(dimensionSMTokenPill).toHaveCount(2);
await dimensionSMTokenPill.nth(1).click();
// Change token from dropdown
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(dimensionSMTokenPill).toHaveCount(1);
const dimensionXLTokenPill = measuresSection.getByRole("button", {
name: "dimension.xl",
});
await expect(dimensionXLTokenPill).toBeVisible();
// Detach token from design tab on right sidebar
const detachButton = measuresSection.getByRole("button", {
name: "Detach token",
});
await detachButton.nth(1).click();
await expect(dimensionXLTokenPill).not.toBeVisible();
});
test("User applies dimension token to a shape on x position", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
// Apply token to width and height token from token panel
await tokensSidebar
.getByRole("button", { name: "dimension.sm" })
.click({ button: "right" });
await tokenContextMenuForToken.getByText("AxisX").click();
// Check if measures sections is visible on right sidebar
const measuresSection = page.getByRole("region", {
name: "shape-measures-section",
});
await expect(measuresSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const dimensionSMTokenPill = measuresSection.getByRole("button", {
name: "dimension.sm",
});
await expect(dimensionSMTokenPill).toBeVisible();
await dimensionSMTokenPill.click();
// Change token from dropdown
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(dimensionSMTokenPill).not.toBeVisible();
const dimensionXLTokenPill = measuresSection.getByRole("button", {
name: "dimension.xl",
});
await expect(dimensionXLTokenPill).toBeVisible();
// Detach token from design tab on right sidebar
const detachButton = measuresSection.getByRole("button", {
name: "Detach token",
});
await detachButton.nth(0).click();
await expect(dimensionXLTokenPill).not.toBeVisible();
});
test("User applies dimension token to a shape on y position", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.sm");
// Apply token to width and height token from token panel
await tokensSidebar
.getByRole("button", { name: "dimension.sm" })
.click({ button: "right" });
await tokenContextMenuForToken.getByText("Y").click();
// Check if measures sections is visible on right sidebar
const measuresSection = page.getByRole("region", {
name: "shape-measures-section",
});
await expect(measuresSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const dimensionSMTokenPill = measuresSection.getByRole("button", {
name: "dimension.sm",
});
await expect(dimensionSMTokenPill).toBeVisible();
await dimensionSMTokenPill.click();
// Change token from dropdown
const dimensionTokenOptionXl = measuresSection.getByLabel("dimension.xl");
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(dimensionSMTokenPill).not.toBeVisible();
const dimensionXLTokenPill = measuresSection.getByRole("button", {
name: "dimension.xl",
});
await expect(dimensionXLTokenPill).toBeVisible();
// Detach token from design tab on right sidebar
const detachButton = measuresSection.getByRole("button", {
name: "Detach token",
});
await detachButton.nth(0).click();
await expect(dimensionXLTokenPill).not.toBeVisible();
});
test("User applies dimension token to a shape border-radius", async ({
page,
}) => {
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
await setupTokensFile(page);
// Unfolds dimensions on token panel
await page.getByRole("tab", { name: "Layers" }).click();
await workspacePage.layers.getByTestId("layer-row").nth(2).click();
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
await tokensTabButton.click();
unfoldTokenTree(tokensSidebar, "dimensions", "dimension.dimension.xs");
// Apply token to width and height token from token panel
await tokensSidebar
.getByRole("button", { name: "dimension.xs" })
.click({ button: "right" });
await tokenContextMenuForToken.getByText("Border radius").hover();
await tokenContextMenuForToken.getByText("RadiusAll").click();
// Check if border radius sections is visible on right sidebar
const borderRadiusSection = page.getByRole("region", {
name: "border-radius-section",
});
await expect(borderRadiusSection).toBeVisible();
// Check if token pill is visible on design tab on right sidebar
const dimensionXSTokenPill = borderRadiusSection.getByRole("button", {
name: "dimension.xs",
});
await expect(dimensionXSTokenPill).toBeVisible();
await dimensionXSTokenPill.click();
// Change token from dropdown
const dimensionTokenOptionXl = borderRadiusSection.getByLabel("dimension.xl");
await expect(dimensionTokenOptionXl).toBeVisible();
await dimensionTokenOptionXl.click();
await expect(dimensionXSTokenPill).not.toBeVisible();
const dimensionXLTokenPill = borderRadiusSection.getByRole("button", {
name: "dimension.xl",
});
await expect(dimensionXLTokenPill).toBeVisible();
// Detach token from design tab on right sidebar
const detachButton = borderRadiusSection.getByRole("button", {
name: "Detach token",
});
await detachButton.nth(0).click();
await expect(dimensionXLTokenPill).not.toBeVisible();
});
}); });

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,8 +27,10 @@
[app.main.data.workspace.colors :as wdc] [app.main.data.workspace.colors :as wdc]
[app.main.data.workspace.shape-layout :as dwsl] [app.main.data.workspace.shape-layout :as dwsl]
[app.main.data.workspace.shapes :as dwsh] [app.main.data.workspace.shapes :as dwsh]
[app.main.data.workspace.texts :as dwt]
[app.main.data.workspace.transforms :as dwtr] [app.main.data.workspace.transforms :as dwtr]
[app.main.data.workspace.undo :as dwu] [app.main.data.workspace.undo :as dwu]
[app.main.features :as features]
[app.main.fonts :as fonts] [app.main.fonts :as fonts]
[app.main.store :as st] [app.main.store :as st]
[app.util.i18n :refer [tr]] [app.util.i18n :refer [tr]]
@@ -191,6 +193,16 @@
(when (:fill attributes) (update-fill value shape-ids attributes page-id)) (when (:fill attributes) (update-fill value shape-ids attributes page-id))
(when (:stroke-color attributes) (update-stroke-color value shape-ids attributes page-id))))))) (when (:stroke-color attributes) (update-stroke-color value shape-ids attributes page-id)))))))
(defn update-shape-dimensions
([value shape-ids attributes] (update-shape-dimensions value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::update-shape-dimensions
ptk/WatchEvent
(watch [_ _ _]
(when (number? value)
(rx/of
(when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id}))
(when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id}))))))))
(defn- attributes->layout-gap [attributes value] (defn- attributes->layout-gap [attributes value]
(let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap}) (let [layout-gap (-> (set/intersection attributes #{:column-gap :row-gap})
@@ -238,6 +250,21 @@
{:ignore-touched true {:ignore-touched true
:page-id page-id})))))))) :page-id page-id}))))))))
(defn update-layout-spacing
([value shape-ids attributes] (update-layout-spacing value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::update-layout-spacing
ptk/WatchEvent
(watch [_ state _]
(when (number? value)
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)
layout-attributes (attributes->layout-gap attributes value)]
(rx/of
(dwsl/update-layout ids-with-layout
layout-attributes
{:ignore-touched true
:page-id page-id}))))))))
(defn update-shape-position (defn update-shape-position
([value shape-ids attributes] (update-shape-position value shape-ids attributes nil)) ([value shape-ids attributes] (update-shape-position value shape-ids attributes nil))
([value shape-ids attributes page-id] ([value shape-ids attributes page-id]
@@ -251,20 +278,6 @@
{:ignore-touched true {:ignore-touched true
:page-id page-id}))))))))) :page-id page-id})))))))))
(defn update-layout-gap
[value shape-ids attributes page-id]
(ptk/reify ::update-layout-gao
ptk/WatchEvent
(watch [_ state _]
(when (number? value)
(let [ids-with-layout (shape-ids-with-layout state (or page-id (:current-page-id state)) shape-ids)
layout-attributes (attributes->layout-gap attributes value)]
(rx/of
(dwsl/update-layout ids-with-layout
layout-attributes
{:ignore-touched true
:page-id page-id})))))))
(defn update-layout-sizing-limits (defn update-layout-sizing-limits
([value shape-ids attributes] (update-layout-sizing-limits value shape-ids attributes nil)) ([value shape-ids attributes] (update-layout-sizing-limits value shape-ids attributes nil))
([value shape-ids attributes page-id] ([value shape-ids attributes page-id]
@@ -289,11 +302,20 @@
update-fn (fn [node _] update-fn (fn [node _]
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node)))] (cty/remove-typography-from-node)))
(dwsh/update-shapes shape-ids ;; Check if any attribute affects text layout (requires resize)
#(txt/update-text-content % update-node? update-fn nil) affects-layout? (some #(contains? txt-attrs %) [:font-size :font-family :font-weight :letter-spacing :line-height])]
{:ignore-touched true (ptk/reify ::generate-text-shape-update
:page-id page-id}))) ptk/WatchEvent
(watch [_ state _]
(cond-> (rx/of (dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id}))
(and affects-layout?
(features/active-feature? state "render-wasm/v1"))
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-line-height (defn update-line-height
([value shape-ids attributes] (update-line-height value shape-ids attributes nil)) ([value shape-ids attributes] (update-line-height value shape-ids attributes nil))
@@ -342,11 +364,17 @@
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))] (cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids (ptk/reify ::generate-font-family-text-shape-update
(fn [shape] ptk/WatchEvent
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil)) (watch [_ state _]
{:ignore-touched true (cond-> (rx/of (dwsh/update-shapes shape-ids
:page-id page-id}))) (fn [shape]
(txt/update-text-content shape update-node? #(update-fn %1 (ctst/font-weight-applied? shape)) nil))
{:ignore-touched true
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn- create-font-family-text-attrs (defn- create-font-family-text-attrs
[value] [value]
@@ -414,10 +442,16 @@
(-> node (-> node
(d/txt-merge txt-attrs) (d/txt-merge txt-attrs)
(cty/remove-typography-from-node))))] (cty/remove-typography-from-node))))]
(dwsh/update-shapes shape-ids (ptk/reify ::generate-font-weight-text-shape-update
#(txt/update-text-content % update-node? update-fn nil) ptk/WatchEvent
{:ignore-touched true (watch [_ state _]
:page-id page-id}))) (cond-> (rx/of (dwsh/update-shapes shape-ids
#(txt/update-text-content % update-node? update-fn nil)
{:ignore-touched true
:page-id page-id}))
(features/active-feature? state "render-wasm/v1")
(rx/merge
(rx/of (dwt/resize-wasm-text-all shape-ids))))))))
(defn update-font-weight (defn update-font-weight
([value shape-ids attributes] (update-font-weight value shape-ids attributes nil)) ([value shape-ids attributes] (update-font-weight value shape-ids attributes nil))
@@ -459,126 +493,20 @@
value value
[shape-ids attributes page-id]))))) [shape-ids attributes page-id])))))
(defn update-shape-dimensions (defn update-typography-interactive
([value shape-ids attributes] (update-shape-dimensions value shape-ids attributes nil)) ([value shape-ids attributes] (update-typography value shape-ids attributes nil))
([value shape-ids attributes page-id] ([value shape-ids attributes page-id]
(ptk/reify ::update-shape-dimensions (when (map? value)
ptk/WatchEvent (rx/merge
(watch [_ _ _] (apply-functions-map
(when (number? value) {:font-size update-font-size
(rx/of :font-family update-font-family-interactive
(when (:width attributes) (dwtr/update-dimensions shape-ids :width value {:ignore-touched true :page-id page-id})) :font-weight update-font-weight-interactive
(when (:height attributes) (dwtr/update-dimensions shape-ids :height value {:ignore-touched true :page-id page-id})))))))) :letter-spacing update-letter-spacing
:text-case update-text-case
(defn- attributes->actions :text-decoration update-text-decoration-interactive}
[{:keys [value shape-ids attributes page-id]}] value
(cond-> [] [shape-ids attributes page-id])))))
(some attributes #{:width :height})
(conj #(update-shape-dimensions
value shape-ids
(set (filter attributes #{:width :height}))
page-id))
(some attributes #{:x :y})
(conj #(update-shape-position
value shape-ids
(set (filter attributes #{:x :y}))
page-id))
(some attributes #{:p1 :p2 :p3 :p4})
(conj #(update-layout-padding
value shape-ids
(set (filter attributes #{:p1 :p2 :p3 :p4}))
page-id))
(some attributes #{:m1 :m2 :m3 :m4})
(conj #(update-layout-item-margin
value shape-ids
(set (filter attributes #{:m1 :m2 :m3 :m4}))
page-id))
(some attributes #{:row-gap :column-gap})
(conj #(update-layout-gap
value shape-ids
(set (filter attributes #{:row-gap :column-gap}))
page-id))
(some attributes #{:r1 :r2 :r3 :r4})
(conj #(if (= attributes #{:r1 :r2 :r3 :r4})
(update-shape-radius-all value shape-ids attributes page-id)
(update-shape-radius-for-corners
value shape-ids
(set (filter attributes #{:r1 :r2 :r3 :r4}))
page-id)))
(some attributes #{:strole-width})
(conj #(update-stroke-width
value shape-ids
#{:strole-width}
page-id))
(some attributes #{:max-width :max-height})
(conj #(update-layout-sizing-limits
value shape-ids
(set (filter attributes #{:max-width :max-height}))
page-id))))
(defn use-dimensions-token
([value shape-ids attributes] (use-dimensions-token value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::use-dimensions-token
ptk/WatchEvent
(watch [_ state _]
(when (number? value)
(let [actions (attributes->actions
{:value value
:shape-ids shape-ids
:attributes attributes
:page-id page-id
:state state})]
(apply rx/of (map #(%) actions))))))))
(defn use-spacing-token
([value shape-ids attributes] (use-spacing-token value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::use-spacing-token
ptk/WatchEvent
(watch [_ state _]
(let [spacing-attrs
#{:row-gap :column-gap
:m1 :m2 :m3 :m4
:p1 :p2 :p3 :p4}]
(when (and (number? value)
(set? attributes)
(set/subset? attributes spacing-attrs))
(let [actions (attributes->actions
{:value value
:shape-ids shape-ids
:attributes attributes
:page-id page-id
:state state})]
(apply rx/of (map #(%) actions)))))))))
(defn use-sizing-token
([value shape-ids attributes] (use-sizing-token value shape-ids attributes nil))
([value shape-ids attributes page-id]
(ptk/reify ::use-sizing-token
ptk/WatchEvent
(watch [_ state _]
(let [sizing-attrs
#{:width :height
:max-width :max-height}]
(when (and (number? value)
(set? attributes)
(set/subset? attributes sizing-attrs))
(let [actions (attributes->actions
{:value value
:shape-ids shape-ids
:attributes attributes
:page-id page-id
:state state})]
(apply rx/of (map #(%) actions)))))))))
;; Events to apply / unapply tokens to shapes ------------------------------------------------------------ ;; Events to apply / unapply tokens to shapes ------------------------------------------------------------
@@ -718,19 +646,54 @@
:token token :token token
:shape-ids shape-ids})) :shape-ids shape-ids}))
(rx/of (rx/of
(cond (case (:type token)
(and (= (:type token) :spacing) :spacing
(nil? attrs))
(apply-spacing-token {:token token (apply-spacing-token {:token token
:attr attrs :attr attrs
:shapes shapes}) :shapes shapes})
:else
(apply-token {:attributes (if (empty? attrs) attributes attrs) (apply-token {:attributes (if (empty? attrs) attributes attrs)
:token token :token token
:shape-ids shape-ids :shape-ids shape-ids
:on-update-shape on-update-shape})))))))) :on-update-shape on-update-shape}))))))))
(defn toggle-border-radius-token
[{:keys [token attrs shape-ids expand-with-children]}]
(ptk/reify ::on-toggle-border-radius-token
ptk/WatchEvent
(watch [_ state _]
(let [objects (dsh/lookup-page-objects state)
shapes (into [] (keep (d/getf objects)) shape-ids)
shapes
(if expand-with-children
(into []
(mapcat (fn [shape]
(if (= (:type shape) :group)
(keep objects (:shapes shape))
[shape])))
shapes)
shapes)
{:keys [attributes all-attributes]}
(get token-properties (:type token))
unapply-tokens?
(cft/shapes-token-applied? token shapes (or attrs all-attributes attributes))
shape-ids (map :id shapes)]
(if unapply-tokens?
(rx/of
(unapply-token {:attributes (or attrs all-attributes attributes)
:token token
:shape-ids shape-ids}))
(rx/of
(apply-token {:attributes attrs
:token token
:shape-ids shape-ids
:on-update-shape update-shape-radius-for-corners})))))))
(defn apply-token-on-selected (defn apply-token-on-selected
[color-operations token] [color-operations token]
(ptk/reify ::apply-token-on-selected (ptk/reify ::apply-token-on-selected
@@ -860,7 +823,7 @@
{:title "Sizing" {:title "Sizing"
:attributes #{:width :height} :attributes #{:width :height}
:all-attributes ctt/sizing-keys :all-attributes ctt/sizing-keys
:on-update-shape use-sizing-token :on-update-shape update-shape-dimensions
:modal {:key :tokens/sizing :modal {:key :tokens/sizing
:fields [{:label "Sizing" :fields [{:label "Sizing"
:key :sizing}]}} :key :sizing}]}}
@@ -873,7 +836,7 @@
ctt/border-radius-keys ctt/border-radius-keys
ctt/axis-keys ctt/axis-keys
ctt/stroke-width-keys) ctt/stroke-width-keys)
:on-update-shape use-dimensions-token :on-update-shape update-shape-dimensions
:modal {:key :tokens/dimensions :modal {:key :tokens/dimensions
:fields [{:label "Dimensions" :fields [{:label "Dimensions"
:key :dimensions}]}} :key :dimensions}]}}
@@ -906,7 +869,7 @@
{:title "Spacing" {:title "Spacing"
:attributes #{:column-gap :row-gap} :attributes #{:column-gap :row-gap}
:all-attributes ctt/spacing-keys :all-attributes ctt/spacing-keys
:on-update-shape use-spacing-token :on-update-shape update-layout-spacing
:modal {:key :tokens/spacing :modal {:key :tokens/spacing
:fields [{:label "Spacing" :fields [{:label "Spacing"
:key :spacing}]}})) :key :spacing}]}}))

View File

@@ -54,7 +54,7 @@
{ctt/border-radius-keys dwta/update-shape-radius-for-corners {ctt/border-radius-keys dwta/update-shape-radius-for-corners
ctt/color-keys dwta/update-fill-stroke ctt/color-keys dwta/update-fill-stroke
ctt/stroke-width-keys dwta/update-stroke-width ctt/stroke-width-keys dwta/update-stroke-width
ctt/sizing-keys dwta/use-dimensions-token ctt/sizing-keys dwta/update-shape-dimensions
ctt/opacity-keys dwta/update-opacity ctt/opacity-keys dwta/update-opacity
ctt/rotation-keys dwta/update-rotation ctt/rotation-keys dwta/update-rotation
@@ -73,8 +73,8 @@
#{:x :y} dwta/update-shape-position #{:x :y} dwta/update-shape-position
#{:p1 :p2 :p3 :p4} dwta/update-layout-padding #{:p1 :p2 :p3 :p4} dwta/update-layout-padding
#{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin #{:m1 :m2 :m3 :m4} dwta/update-layout-item-margin
#{:column-gap :row-gap} dwta/update-layout-gap #{:column-gap :row-gap} dwta/update-layout-spacing
#{:width :height} dwta/use-dimensions-token #{:width :height} dwta/update-shape-dimensions
#{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits}) #{:layout-item-min-w :layout-item-min-h :layout-item-max-w :layout-item-max-h} dwta/update-layout-sizing-limits})
(def ^:private attribute-actions-map (def ^:private attribute-actions-map

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,26 @@
{ /* This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
Copyright (c) KALEIDOS INC */ }
import { Canvas, Meta } from '@storybook/addon-docs/blocks';
import * as PanelTitle from "./panel_title.stories";
<Meta title="Product/PanelTitle" />
# PanelTitle
The `panel-title*` is used as a header for some sidebar sections.
<Canvas of={PanelTitle.Default} />
## Technical notes
The only mandatory parameter is `text`. Usually you'll want to pass a function property `on-close` that will be called when the user clicks on the close button on the right.
```clj
[:> panel-title* {:class class
:text text
:on-close on-close}]
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -192,10 +192,11 @@
(st/emit! (st/emit!
(change-radius (fn [shape] (change-radius (fn [shape]
(ctsr/set-radius-to-all-corners shape value)))) (ctsr/set-radius-to-all-corners shape value))))
(st/emit! (doseq [attr [:r1 :r2 :r3 :r4]]
(dwta/toggle-token {:token (first value) (st/emit!
:attrs #{:r1 :r2 :r3 :r4} (dwta/toggle-token {:token (first value)
:shape-ids ids}))))) :attrs #{attr}
:shape-ids ids}))))))
on-single-radius-change on-single-radius-change
@@ -204,10 +205,9 @@
(fn [value attr] (fn [value attr]
(if (or (string? value) (number? value)) (if (or (string? value) (number? value))
(st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr)) (st/emit! (change-one-radius #(ctsr/set-radius-to-single-corner % attr value) attr))
(st/emit! (st/emit! (st/emit! (dwta/toggle-border-radius-token {:token (first value)
(dwta/toggle-token {:token (first value) :attrs #{attr}
:attrs #{attr} :shape-ids ids})))))
:shape-ids ids}))))))
on-radius-r1-change #(on-single-radius-change % :r1) on-radius-r1-change #(on-single-radius-change % :r1)
on-radius-r2-change #(on-single-radius-change % :r2) on-radius-r2-change #(on-single-radius-change % :r2)

View File

@@ -369,12 +369,12 @@
(if (or (string? value) (int? value)) (if (or (string? value) (int? value))
(on-change :simple attr value event) (on-change :simple attr value event)
(do (do
(st/emit! (let [resolved-value (:resolved-value (first value))
(dwta/toggle-token {:token (first value) updated-attr (if (= :p1 attr) #{:p1 :p3} #{:p2 :p4})]
:attrs (if (= :p1 attr) (st/emit! (dwta/toggle-token {:token (first value)
#{:p1 :p3} :attrs updated-attr
#{:p2 :p4}) :shape-ids ids}))
:shape-ids ids})))))) (on-change :simple attr resolved-value event))))))
on-detach-token on-detach-token
(mf/use-fn (mf/use-fn
@@ -483,9 +483,11 @@
(if (or (string? value) (int? value)) (if (or (string? value) (int? value))
(on-change :multiple attr value event) (on-change :multiple attr value event)
(do (do
(st/emit! (dwta/toggle-token {:token (first value) (let [resolved-value (:resolved-value (first value))]
:attrs #{attr} (st/emit! (dwta/toggle-token {:token (first value)
:shape-ids ids})))))) :attrs #{attr}
:shape-ids ids}))
(on-change :multiple attr resolved-value event))))))
on-focus on-focus
(mf/use-fn (mf/use-fn
@@ -714,12 +716,11 @@
(if (or (string? value) (int? value)) (if (or (string? value) (int? value))
(on-change (= "nowrap" wrap-type) attr value event) (on-change (= "nowrap" wrap-type) attr value event)
(do (do
(st/emit! (let [resolved-value (:resolved-value (first value))]
(dwta/toggle-token {:token (first value) (st/emit! (dwta/toggle-token {:token (first value)
:attrs (if (= "nowrap" wrap-type) :attrs #{attr}
#{:row-gap :colum-gap} :shape-ids ids}))
#{attr}) (on-change (= "nowrap" wrap-type) attr resolved-value event))))))
:shape-ids ids}))))))
on-detach-token on-detach-token
(mf/use-fn (mf/use-fn

View File

@@ -284,17 +284,28 @@
(st/emit! (udw/change-orientation ids (keyword orientation))))) (st/emit! (udw/change-orientation ids (keyword orientation)))))
;; SIZE AND PROPORTION LOCK ;; SIZE AND PROPORTION LOCK
do-size-change
(mf/use-fn
(mf/deps ids)
(fn [value attr]
(st/emit! (udw/trigger-bounding-box-cloaking ids)
(udw/update-dimensions ids attr value))))
on-size-change on-size-change
(mf/use-fn (mf/use-fn
(mf/deps ids shapes) (mf/deps ids shapes)
(fn [value attr] (fn [value attr]
(if (or (string? value) (number? value)) (if (or (string? value) (number? value))
(st/emit! (udw/trigger-bounding-box-cloaking ids) (do
(udw/update-dimensions ids attr value)) (st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/trigger-bounding-box-cloaking ids) (run! #(do-size-change value attr) shapes))
(dwta/toggle-token {:token (first value) (do
:attrs #{attr} (let [resolved-value (:resolved-value (first value))]
:shape-ids ids}))))) (st/emit! (udw/trigger-bounding-box-cloaking ids)
(dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))
(run! #(do-size-change resolved-value attr) shapes))))))
on-proportion-lock-change on-proportion-lock-change
(mf/use-fn (mf/use-fn
@@ -304,6 +315,11 @@
(run! #(st/emit! (udw/set-shape-proportion-lock % new-lock)) ids)))) (run! #(st/emit! (udw/set-shape-proportion-lock % new-lock)) ids))))
;; POSITION ;; POSITION
do-position-change
(mf/use-fn
(fn [shape' value attr]
(st/emit! (udw/update-position (:id shape') {attr value}))))
on-position-change on-position-change
(mf/use-fn (mf/use-fn
(mf/deps ids) (mf/deps ids)
@@ -311,11 +327,21 @@
(if (or (string? value) (number? value)) (if (or (string? value) (number? value))
(do (do
(st/emit! (udw/trigger-bounding-box-cloaking ids)) (st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/update-position ids {attr value}))) (run! #(do-position-change %1 value attr) shapes))
(st/emit! (udw/trigger-bounding-box-cloaking ids) (do
(dwta/toggle-token {:token (first value) (let [resolved-value (:resolved-value (first value))]
:attrs #{attr} (st/emit! (udw/trigger-bounding-box-cloaking ids)
:shape-ids ids}))))) (dwta/toggle-token {:token (first value)
:attrs #{attr}
:shape-ids ids}))
(run! #(do-position-change %1 resolved-value attr) shapes))))))
;; ROTATION
do-rotation-change
(mf/use-fn
(mf/deps ids)
(fn [value]
(st/emit! (udw/increase-rotation ids value))))
on-rotation-change on-rotation-change
(mf/use-fn (mf/use-fn
@@ -324,11 +350,14 @@
(if (or (string? value) (number? value)) (if (or (string? value) (number? value))
(do (do
(st/emit! (udw/trigger-bounding-box-cloaking ids)) (st/emit! (udw/trigger-bounding-box-cloaking ids))
(st/emit! (udw/increase-rotation ids value))) (run! #(do-rotation-change value) shapes))
(st/emit! (udw/trigger-bounding-box-cloaking ids) (do
(dwta/toggle-token {:token (first value) (let [resolved-value (:resolved-value (first value))]
:attrs #{:rotation} (st/emit! (udw/trigger-bounding-box-cloaking ids)
:shape-ids ids}))))) (dwta/toggle-token {:token (first value)
:attrs #{:rotation}
:shape-ids ids}))
(run! #(do-rotation-change resolved-value) shapes))))))
on-width-change on-width-change
(mf/use-fn (mf/deps on-size-change) #(on-size-change % :width)) (mf/use-fn (mf/deps on-size-change) #(on-size-change % :width))
@@ -381,8 +410,7 @@
(fn [] (fn []
(st/emit! (dwt/selected-fit-content))))] (st/emit! (dwt/selected-fit-content))))]
[:section {:class (stl/css :element-set) [:div {:class (stl/css :element-set)}
:aria-label "shape-measures-section"}
(when (and (options :presets) (when (and (options :presets)
(or (nil? all-types) (= (count all-types) 1))) (or (nil? all-types) (= (count all-types) 1)))
[:div {:class (stl/css :presets)} [:div {:class (stl/css :presets)}

View File

@@ -9,50 +9,18 @@
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.types.color :as ctc] [app.common.types.color :as ctc]
[app.common.types.token :as tk]
[app.main.data.workspace.tokens.application :as dwta] [app.main.data.workspace.tokens.application :as dwta]
[app.main.features :as features]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.numeric-input :as deprecated-input] [app.main.ui.components.numeric-input :refer [numeric-input*]]
[app.main.ui.components.reorder-handler :refer [reorder-handler*]] [app.main.ui.components.reorder-handler :refer [reorder-handler*]]
[app.main.ui.components.select :refer [select]] [app.main.ui.components.select :refer [select]]
[app.main.ui.context :as muc]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]] [app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
[app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i] [app.main.ui.ds.foundations.assets.icon :refer [icon*] :as i]
[app.main.ui.hooks :as h] [app.main.ui.hooks :as h]
[app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row*]]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(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)
props (mf/spread-props props
{:placeholder (if (= :multiple values)
(tr "settings.multiple")
"--")
:applied-token applied-token
:tokens (if (delay? tokens) @tokens tokens)
:align align
:on-detach on-detach-attr
:name name
:value values})]
[:> numeric-input* props]))
(mf/defc stroke-row* (mf/defc stroke-row*
[{:keys [index [{:keys [index
stroke stroke
@@ -77,10 +45,7 @@
select-on-focus select-on-focus
ids]}] ids]}]
(let [token-numeric-inputs (let [on-drop
(features/use-feature "tokens/numeric-input")
on-drop
(mf/use-fn (mf/use-fn
(mf/deps on-reorder index) (mf/deps on-reorder index)
(fn [relative-pos data] (fn [relative-pos data]
@@ -123,13 +88,7 @@
on-width-change on-width-change
(mf/use-fn (mf/use-fn
(mf/deps index on-stroke-width-change) (mf/deps index on-stroke-width-change)
(fn [value] #(on-stroke-width-change index %))
(if (or (string? value) (int? value))
(on-stroke-width-change index value)
(do
(st/emit! (dwta/toggle-token {:token (first value)
:attrs #{:stroke-width}
:shape-ids ids}))))))
stroke-alignment (or (:stroke-alignment stroke) :center) stroke-alignment (or (:stroke-alignment stroke) :center)
@@ -190,12 +149,6 @@
(fn [token] (fn [token]
(on-detach-token token #{:stroke-color}))) (on-detach-token token #{:stroke-color})))
on-detach-token-width
(mf/use-fn
(mf/deps on-detach-token)
(fn [token]
(on-detach-token (first token) #{:stroke-width})))
stroke-caps-options stroke-caps-options
[{:value nil :label (tr "workspace.options.stroke-cap.none")} [{:value nil :label (tr "workspace.options.stroke-cap.none")}
:separator :separator
@@ -242,30 +195,17 @@
;; Stroke Width, Alignment & Style ;; Stroke Width, Alignment & Style
[:div {:class (stl/css :stroke-options)} [:div {:class (stl/css :stroke-options)}
(if token-numeric-inputs [:div {:class (stl/css :stroke-width-input)
[:> numeric-input-wrapper* {:on-change on-width-change :title (tr "workspace.options.stroke-width")}
:on-detach on-detach-token-width [:> icon* {:icon-id i/stroke-size
:icon i/stroke-size :size "s"}]
:min 0 [:> numeric-input* {:value stroke-width
:on-focus on-focus :min 0
:on-blur on-blur :placeholder (tr "settings.multiple")
:name :stroke-width :on-change on-width-change
:class (stl/css :numeric-input-wrapper) :on-focus on-focus
:property (tr "workspace.options.stroke-width") :select-on-focus select-on-focus
:applied-tokens applied-tokens :on-blur on-blur}]]
:values stroke-width}]
[:div {:class (stl/css :stroke-width-input)
:title (tr "workspace.options.stroke-width")}
[:> icon* {:icon-id i/stroke-size
:size "s"}]
[:> deprecated-input/numeric-input* {:value stroke-width
:min 0
:placeholder (tr "settings.multiple")
:on-change on-width-change
:on-focus on-focus
:select-on-focus select-on-focus
:on-blur on-blur}]])
[:div {:class (stl/css :stroke-alignment-select) [:div {:class (stl/css :stroke-alignment-select)
:data-testid "stroke.alignment"} :data-testid "stroke.alignment"}

View File

@@ -45,11 +45,6 @@
padding-inline-start: var(--sp-xs); padding-inline-start: var(--sp-xs);
} }
.numeric-input-wrapper {
grid-column: span 2;
--dropdown-width: var(--7-columns-dropdown-width);
}
.stroke-alignment-select { .stroke-alignment-select {
grid-column: span 3; grid-column: span 3;
} }

View File

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

View File

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

View File

@@ -223,7 +223,7 @@
gap-items (all-or-separate-actions {:attribute-labels {:column-gap "Column Gap" gap-items (all-or-separate-actions {:attribute-labels {:column-gap "Column Gap"
:row-gap "Row Gap"} :row-gap "Row Gap"}
:hint (tr "workspace.tokens.gaps") :hint (tr "workspace.tokens.gaps")
:on-update-shape dwta/update-layout-gap} :on-update-shape dwta/update-layout-spacing}
context-data)] context-data)]
(->> (concat (->> (concat
gap-items gap-items
@@ -239,7 +239,7 @@
(all-or-separate-actions {:attribute-labels {:width "Width" (all-or-separate-actions {:attribute-labels {:width "Width"
:height "Height"} :height "Height"}
:hint (tr "workspace.tokens.size") :hint (tr "workspace.tokens.size")
:on-update-shape dwta/use-dimensions-token} :on-update-shape dwta/update-shape-dimensions}
context-data) context-data)
[:separator] [:separator]
(all-or-separate-actions {:attribute-labels {:layout-item-min-w "Min Width" (all-or-separate-actions {:attribute-labels {:layout-item-min-w "Min Width"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -260,7 +260,7 @@
events [(dwta/apply-token {:shape-ids [(:id rect-1)] events [(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:width :height} :attributes #{:width :height}
:token (toht/get-token file "dimensions.sm") :token (toht/get-token file "dimensions.sm")
:on-update-shape dwta/use-dimensions-token})]] :on-update-shape dwta/update-shape-dimensions})]]
(tohs/run-store-async (tohs/run-store-async
store done events store done events
(fn [new-state] (fn [new-state]
@@ -333,7 +333,7 @@
events [(dwta/apply-token {:shape-ids [(:id rect-1)] events [(dwta/apply-token {:shape-ids [(:id rect-1)]
:attributes #{:width :height} :attributes #{:width :height}
:token (toht/get-token file "sizing.sm") :token (toht/get-token file "sizing.sm")
:on-update-shape dwta/use-dimensions-token})]] :on-update-shape dwta/update-shape-dimensions})]]
(tohs/run-store-async (tohs/run-store-async
store done events store done events
(fn [new-state] (fn [new-state]

View File

@@ -5476,6 +5476,10 @@ msgstr "Delete row and shapes"
msgid "workspace.context-menu.grid-track.row.duplicate" msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicate row" msgstr "Duplicate row"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512 #: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode" msgid "workspace.focus.focus-mode"
msgstr "Focus mode" msgstr "Focus mode"
@@ -8421,6 +8425,10 @@ msgstr "Comments (%s)"
msgid "workspace.toolbar.curve" msgid "workspace.toolbar.curve"
msgstr "Curve (%s)" msgstr "Curve (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Debugging tools"
#: src/app/main/ui/workspace/top_toolbar.cljs:172 #: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse" msgid "workspace.toolbar.ellipse"
msgstr "Ellipse (%s)" msgstr "Ellipse (%s)"

View File

@@ -5461,6 +5461,10 @@ msgstr "Borrar fila con el contenido"
msgid "workspace.context-menu.grid-track.row.duplicate" msgid "workspace.context-menu.grid-track.row.duplicate"
msgstr "Duplicar fila" msgstr "Duplicar fila"
#: src/app/main/ui/workspace/sidebar/debug.cljs:37
msgid "workspace.debug.title"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/sidebar/layers.cljs:512 #: src/app/main/ui/workspace/sidebar/layers.cljs:512
msgid "workspace.focus.focus-mode" msgid "workspace.focus.focus-mode"
msgstr "Modo foco" msgstr "Modo foco"
@@ -7965,7 +7969,7 @@ msgstr "Line height (multiplicador, px o %) o {alias}"
#: src/app/main/data/workspace/tokens/errors.cljs:57 #: src/app/main/data/workspace/tokens/errors.cljs:57
msgid "workspace.tokens.missing-references" msgid "workspace.tokens.missing-references"
msgstr "Referéncias de tokens no encontradas:" msgstr "Referencias de tokens no encontradas: "
#: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123 #: src/app/main/ui/workspace/tokens/management/token_pill.cljs:123
msgid "workspace.tokens.more-options" msgid "workspace.tokens.more-options"
@@ -8282,6 +8286,10 @@ msgstr "Comentarios (%s)"
msgid "workspace.toolbar.curve" msgid "workspace.toolbar.curve"
msgstr "Curva (%s)" msgstr "Curva (%s)"
#: src/app/main/ui/workspace/top_toolbar.cljs:231
msgid "workspace.toolbar.debug"
msgstr "Herramientas de depuración"
#: src/app/main/ui/workspace/top_toolbar.cljs:172 #: src/app/main/ui/workspace/top_toolbar.cljs:172
msgid "workspace.toolbar.ellipse" msgid "workspace.toolbar.ellipse"
msgstr "Elipse (%s)" msgstr "Elipse (%s)"

View File

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

View File

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

View File

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

View File

@@ -1529,6 +1529,7 @@ impl Shape {
|| !self.transform.is_identity() || !self.transform.is_identity()
|| !math::is_close_to(self.rotation, 0.0) || !math::is_close_to(self.rotation, 0.0)
|| matches!(self.shape_type, Type::Group(_) | Type::Frame(_)) || matches!(self.shape_type, Type::Group(_) | Type::Frame(_))
|| matches!(self.shape_type, Type::Text(_))
} }
pub fn count_visible_inner_strokes(&self) -> usize { pub fn count_visible_inner_strokes(&self) -> usize {

View File

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