Compare commits
2 Commits
develop
...
eva-replac
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
71818c4b2b | ||
|
|
df1c8b6388 |
2
.github/workflows/build-docker-devenv.yml
vendored
@@ -7,7 +7,7 @@ jobs:
|
|||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and push DevEnv Docker image
|
name: Build and push DevEnv Docker image
|
||||||
environment: release-admins
|
environment: release-admins
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
2
.github/workflows/build-docker.yml
vendored
@@ -19,7 +19,7 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
build-and-push:
|
build-and-push:
|
||||||
name: Build and Push Penpot Docker Images
|
name: Build and Push Penpot Docker Images
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04-arm
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
24
.github/workflows/tests.yml
vendored
@@ -21,7 +21,7 @@ concurrency:
|
|||||||
jobs:
|
jobs:
|
||||||
lint:
|
lint:
|
||||||
name: "Linter"
|
name: "Linter"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -34,7 +34,7 @@ jobs:
|
|||||||
|
|
||||||
test-common:
|
test-common:
|
||||||
name: "Common Tests"
|
name: "Common Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
test-plugins:
|
test-plugins:
|
||||||
name: Plugins Runtime Linter & Tests
|
name: Plugins Runtime Linter & Tests
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -98,7 +98,7 @@ jobs:
|
|||||||
|
|
||||||
test-frontend:
|
test-frontend:
|
||||||
name: "Frontend Tests"
|
name: "Frontend Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -119,7 +119,7 @@ jobs:
|
|||||||
|
|
||||||
test-render-wasm:
|
test-render-wasm:
|
||||||
name: "Render WASM Tests"
|
name: "Render WASM Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -143,7 +143,7 @@ jobs:
|
|||||||
|
|
||||||
test-backend:
|
test-backend:
|
||||||
name: "Backend Tests"
|
name: "Backend Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -182,7 +182,7 @@ jobs:
|
|||||||
|
|
||||||
test-library:
|
test-library:
|
||||||
name: "Library Tests"
|
name: "Library Tests"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -196,7 +196,7 @@ jobs:
|
|||||||
|
|
||||||
build-integration:
|
build-integration:
|
||||||
name: "Build Integration Bundle"
|
name: "Build Integration Bundle"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -217,7 +217,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-1:
|
test-integration-1:
|
||||||
name: "Integration Tests 1/4"
|
name: "Integration Tests 1/4"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-2:
|
test-integration-2:
|
||||||
name: "Integration Tests 2/4"
|
name: "Integration Tests 2/4"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-3:
|
test-integration-3:
|
||||||
name: "Integration Tests 3/4"
|
name: "Integration Tests 3/4"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
@@ -307,7 +307,7 @@ jobs:
|
|||||||
|
|
||||||
test-integration-4:
|
test-integration-4:
|
||||||
name: "Integration Tests 4/4"
|
name: "Integration Tests 4/4"
|
||||||
runs-on: penpot-runner-02
|
runs-on: ubuntu-24.04
|
||||||
container: penpotapp/devenv:latest
|
container: penpotapp/devenv:latest
|
||||||
needs: build-integration
|
needs: build-integration
|
||||||
|
|
||||||
|
|||||||
@@ -13,14 +13,10 @@
|
|||||||
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
|
- Remap references when renaming tokens [Taiga #10202](https://tree.taiga.io/project/penpot/us/10202)
|
||||||
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
|
- Tokens panel nested path view [Taiga #9966](https://tree.taiga.io/project/penpot/us/9966)
|
||||||
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
|
- Improve usability of lock and hide buttons in the layer panel. [Taiga #12916](https://tree.taiga.io/project/penpot/issue/12916)
|
||||||
- Optimize sidebar performance for deeply nested shapes [Taiga #13017](https://tree.taiga.io/project/penpot/task/13017)
|
|
||||||
|
|
||||||
### :bug: Bugs fixed
|
### :bug: Bugs fixed
|
||||||
|
|
||||||
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
|
- Fix prototype connections lost when switching between variants [Taiga #12812](https://tree.taiga.io/project/penpot/issue/12812)
|
||||||
- Fix wrong image in the onboarding invitation block [Taiga #13040](https://tree.taiga.io/project/penpot/issue/13040)
|
|
||||||
- Fix wrong register image [Taiga #12955](https://tree.taiga.io/project/penpot/task/12955)
|
|
||||||
- Fix error message on components doesn't close automatically [Taiga #12012](https://tree.taiga.io/project/penpot/issue/12012)
|
|
||||||
|
|
||||||
## 2.13.0 (Unreleased)
|
## 2.13.0 (Unreleased)
|
||||||
|
|
||||||
|
|||||||
@@ -526,25 +526,20 @@
|
|||||||
ids))
|
ids))
|
||||||
|
|
||||||
(defn clean-loops
|
(defn clean-loops
|
||||||
"Clean a list of ids from circular references. Optimized fast-path for single selections."
|
"Clean a list of ids from circular references."
|
||||||
[objects ids]
|
[objects ids]
|
||||||
(if (<= (count ids) 1)
|
(let [parent-selected?
|
||||||
;; For single selection, there can't be circularity; return as ordered-set.
|
(fn [id]
|
||||||
(into (d/ordered-set) ids)
|
(let [parents (get-parent-ids objects id)]
|
||||||
(let [ids-set (if (set? ids) ids (set ids))
|
(some ids parents)))
|
||||||
parent-selected?
|
|
||||||
(fn [id]
|
|
||||||
;; Stop early as soon as we find any selected parent
|
|
||||||
(let [parents (get-parent-ids objects id)]
|
|
||||||
(some #(contains? ids-set %) parents)))
|
|
||||||
|
|
||||||
add-element
|
add-element
|
||||||
(fn [result id]
|
(fn [result id]
|
||||||
(cond-> result
|
(cond-> result
|
||||||
(not (parent-selected? id))
|
(not (parent-selected? id))
|
||||||
(conj id)))]
|
(conj id)))]
|
||||||
|
|
||||||
(reduce add-element (d/ordered-set) ids))))
|
(reduce add-element (d/ordered-set) ids)))
|
||||||
|
|
||||||
(defn- indexed-shapes
|
(defn- indexed-shapes
|
||||||
"Retrieves a vector with the indexes for each element in the layer
|
"Retrieves a vector with the indexes for each element in the layer
|
||||||
|
|||||||
@@ -487,6 +487,7 @@
|
|||||||
:vertical-margin #{:spacing :dimensions}
|
:vertical-margin #{:spacing :dimensions}
|
||||||
:sided-margins #{:spacing :dimensions}
|
:sided-margins #{:spacing :dimensions}
|
||||||
:line-height #{:line-height :number}
|
:line-height #{:line-height :number}
|
||||||
|
:opacity #{:opacity}
|
||||||
:font-size #{:font-size}
|
:font-size #{:font-size}
|
||||||
:letter-spacing #{:letter-spacing}
|
:letter-spacing #{:letter-spacing}
|
||||||
:fill #{:color}
|
:fill #{:color}
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]
|
<< : [*penpot-flags, *penpot-public-uri, *penpot-http-body-size, *penpot-secret-key]
|
||||||
|
|
||||||
|
## The PREPL host. Mainly used for external programatic access to penpot backend
|
||||||
|
## (example: admin). By default it will listen on `localhost` but if you are going to use
|
||||||
|
## the `admin`, you will need to uncomment this and set the host to `0.0.0.0`.
|
||||||
|
|
||||||
|
# PENPOT_PREPL_HOST: 0.0.0.0
|
||||||
|
|
||||||
## Database connection parameters. Don't touch them unless you are using custom
|
## Database connection parameters. Don't touch them unless you are using custom
|
||||||
## postgresql connection parameters.
|
## postgresql connection parameters.
|
||||||
|
|
||||||
@@ -145,8 +151,8 @@ services:
|
|||||||
## Default configuration for assets storage: using filesystem based with all files
|
## Default configuration for assets storage: using filesystem based with all files
|
||||||
## stored in a docker volume.
|
## stored in a docker volume.
|
||||||
|
|
||||||
PENPOT_OBJECTS_STORAGE_BACKEND: fs
|
PENPOT_ASSETS_STORAGE_BACKEND: assets-fs
|
||||||
PENPOT_OBJECTS_STORAGE_FS_DIRECTORY: /opt/data/assets
|
PENPOT_STORAGE_ASSETS_FS_DIRECTORY: /opt/data/assets
|
||||||
|
|
||||||
## Also can be configured to to use a S3 compatible storage.
|
## Also can be configured to to use a S3 compatible storage.
|
||||||
|
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ http {
|
|||||||
location / {
|
location / {
|
||||||
include /etc/nginx/overrides/location.d/*.conf;
|
include /etc/nginx/overrides/location.d/*.conf;
|
||||||
|
|
||||||
location ~* \.(js|css|wasm|jpg|png|map|svg|ttf|woff|woff2|)$ {
|
location ~* \.(js|css|jpg|png|svg|ttf|woff|woff2|wasm)$ {
|
||||||
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
add_header Cache-Control "public, max-age=604800" always; # 7 days
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,10 +152,8 @@ http {
|
|||||||
return 301 " /404";
|
return 301 " /404";
|
||||||
}
|
}
|
||||||
|
|
||||||
add_header X-Frame-Options SAMEORIGIN always;
|
|
||||||
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
add_header Cache-Control "no-store, no-cache, max-age=0" always;
|
||||||
try_files $uri /index.html$is_args$args /index.html =404;
|
try_files $uri /index.html$is_args$args /index.html =404;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 161 KiB |
|
Before Width: | Height: | Size: 103 KiB |
|
Before Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 6.0 KiB |
|
Before Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 5.2 KiB |
@@ -19,12 +19,6 @@ desc: Begin with the Penpot user guide! Get quickstarts, shortcuts, and tutorial
|
|||||||
<p>Create and manage your teams</p>
|
<p>Create and manage your teams</p>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
|
||||||
<a href="/user-guide/account-teams/projects-files">
|
|
||||||
<h2>Projects and Files →</h2>
|
|
||||||
<p>Organize your work with projects and files</p>
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
<li>
|
||||||
<a href="/user-guide/account-teams/comments/">
|
<a href="/user-guide/account-teams/comments/">
|
||||||
<h2>Comments →</h2>
|
<h2>Comments →</h2>
|
||||||
|
|||||||
@@ -1,107 +0,0 @@
|
|||||||
---
|
|
||||||
title: Projects and Files
|
|
||||||
order: 3
|
|
||||||
desc: Learn how to organize your work in Penpot. Create, manage and organize projects and files, work with drafts, and handle deleted items.
|
|
||||||
---
|
|
||||||
|
|
||||||
<h1 id="projects-files">Projects and Files</h1>
|
|
||||||
<p class="main-paragraph">Projects and files are the core organizational structure in Penpot. Projects work like folders that contain multiple design files, helping you organize your work efficiently. Files are your actual design documents where you create boards, pages, and all your design elements.</p>
|
|
||||||
<p class="main-paragraph">Understanding how to manage projects and files will help you keep your workspace organized and make it easier to collaborate with your team.</p>
|
|
||||||
|
|
||||||
<h2 id="projects-management">Projects</h2>
|
|
||||||
<p>Projects are containers that help you organize and group related design files together. Think of them as folders in a file system. You can create as many projects as you need to organize your work by client, product, feature, or any other structure that fits your workflow.</p>
|
|
||||||
<p>If you're working with others, projects should be created inside a team so that team members can collaborate on the files within them. Projects created in your personal space ("Your Penpot") remain private to you.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/01-projects.webp" alt="Projects view in dashboard" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<h3 id="create-project">Create a project</h3>
|
|
||||||
<p>To create a new project, use the <strong>+ New project</strong> button in the dashboard. You can also use the keyboard shortcut <kbd>+</kbd> when you're on the dashboard. A dialog will appear where you can enter the project name. Once created, the project will appear in your projects list.</p>
|
|
||||||
<p>When you create a project, you can immediately start adding files to it, or create files first and move them into the project later.</p>
|
|
||||||
|
|
||||||
<h3 id="edit-project">Edit a project</h3>
|
|
||||||
<p>To edit a project's name, right-click on the project in the sidebar or click the three-dot menu next to the project name. Select <strong>Edit</strong> or <strong>Rename</strong> to change the project name. You can also update the project's profile picture from the same menu.</p>
|
|
||||||
|
|
||||||
<h3 id="pin-project">Pin a project</h3>
|
|
||||||
<p>Projects can be pinned to the sidebar for quick access. Right-click on a project and select <strong>Pin</strong> to keep it visible in the sidebar even when you have many projects. Pinned projects appear at the top of your projects list for easy access.</p>
|
|
||||||
<p>To unpin a project, right-click on it and select <strong>Unpin</strong>. The project will remain in your list but won't be pinned to the sidebar anymore.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/04-pin-project.webp" alt="Pin project option" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<h3 id="move-project">Move a project</h3>
|
|
||||||
<p>Projects can be moved between teams. To move a project, right-click on it and select <strong>Move to</strong> from the context menu. A dialog will appear showing all available teams where you can move the project. Select the destination team and confirm the move.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/06-move-project.webp" alt="Move project to another team" />
|
|
||||||
</figure>
|
|
||||||
<p>When you move a project to another team, all files within the project are moved along with it. Team members of the destination team will gain access to the project and its files according to their permissions.</p>
|
|
||||||
<p class="advice">Moving a project to another team changes its ownership and access permissions. Make sure the destination team has the appropriate members and permissions for the work contained in the project.</p>
|
|
||||||
|
|
||||||
<h3 id="delete-project">Delete a project</h3>
|
|
||||||
<p>To delete a project, right-click on it and select <strong>Delete</strong> from the menu. You'll be asked to confirm the deletion. Keep in mind that deleting a project will also delete all files within it. Make sure you have backed up any important files before deleting a project.</p>
|
|
||||||
<p class="advice">Deleted projects and their files are moved to the trash area where they can be restored or permanently deleted.</p>
|
|
||||||
|
|
||||||
<h2 id="files-management">Files</h2>
|
|
||||||
<p>Files are your design documents in Penpot. Each file contains pages, boards, and all the design elements you create. Files can be created within a project or in the drafts section, and you can move them between projects as needed.</p>
|
|
||||||
|
|
||||||
<h3 id="create-file">Create a file</h3>
|
|
||||||
<p>To create a new file, you have several options:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Click the <strong>+</strong> button in a project to create a file inside that project</li>
|
|
||||||
<li>Use the keyboard shortcut <kbd>+</kbd> when you have a project selected</li>
|
|
||||||
<li>Create a file directly in the drafts section if you're not ready to organize it into a project yet</li>
|
|
||||||
</ul>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/05-create-file.webp" alt="Create a new file" />
|
|
||||||
</figure>
|
|
||||||
<p>When creating a file, you'll be asked to give it a name. The file will open in the workspace where you can start designing immediately.</p>
|
|
||||||
|
|
||||||
<h3 id="edit-file">Edit a file</h3>
|
|
||||||
<p>To rename a file, right-click on the file card in the dashboard and select <strong>Rename</strong>, or click on the three-dot menu on the file card. Enter the new name and confirm the change. You can also access file settings and other options from the file's context menu.</p>
|
|
||||||
|
|
||||||
<h3 id="move-file">Move a file</h3>
|
|
||||||
<p>Files can be moved between projects, from drafts to a project, or even to projects in other teams. To move a file, right-click on the file card and select <strong>Move to</strong> from the context menu. A dialog will appear showing all available projects across your teams where you can choose the destination. Select the project where you want to move the file and confirm.</p>
|
|
||||||
<p>You can also drag and drop files between projects in the dashboard for a quick way to reorganize your files within the same team.</p>
|
|
||||||
<p>When moving a file to a project in another team, the file becomes accessible to members of that team according to their permissions. Moving a file doesn't affect its content or any shared libraries it might be using. Only its location in your project structure changes.</p>
|
|
||||||
<p class="advice">When moving files between teams, be aware that this changes who has access to the file. Make sure the destination team has the appropriate members and permissions for the work contained in the file.</p>
|
|
||||||
|
|
||||||
<h3 id="duplicate-file">Duplicate a file</h3>
|
|
||||||
<p>To create a copy of an existing file, right-click on the file card and select <strong>Duplicate</strong>. The duplicated file will be created in the same location (project or drafts) with the same name plus "Copy" added to it. You can then rename or move it as needed.</p>
|
|
||||||
<p>Duplicating a file creates a complete copy including all pages, boards, and design elements. This is useful when you want to create variations of a design or use a file as a starting point for a new project.</p>
|
|
||||||
|
|
||||||
<h3 id="delete-file">Delete a file</h3>
|
|
||||||
<p>To delete a file, right-click on the file card and select <strong>Delete</strong>. You'll be asked to confirm the deletion. The file will be moved to the trash area where it can be restored or permanently deleted later.</p>
|
|
||||||
<p class="advice">Deleting a file doesn't immediately remove it permanently. You can recover deleted files from the trash area within a certain time period.</p>
|
|
||||||
|
|
||||||
<h2 id="drafts">Drafts</h2>
|
|
||||||
<p>The drafts section is a fixed, non-deletable space in your dashboard where you can create and store files that aren't part of any specific project yet. This is useful for quick sketches, experimental designs, or files you're not ready to organize into projects.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/02-drafts.webp" alt="Drafts section" />
|
|
||||||
</figure>
|
|
||||||
<p>Drafts appear in a dedicated section in the dashboard sidebar, separate from your projects. All team members can see and access files in the drafts section, depending on their permissions.</p>
|
|
||||||
<p>You can create files directly in drafts, or move existing files from projects into drafts if you want to temporarily remove them from a project's organization. Files in drafts work exactly like files in projects - they have the same functionality and features.</p>
|
|
||||||
<p>When you're ready to organize a file from drafts, you can move it into an appropriate project using the move option in the file's context menu.</p>
|
|
||||||
|
|
||||||
<h2 id="trash-area">Trash area</h2>
|
|
||||||
<p>When you delete projects or files, they are not removed permanently. Instead, they are moved to a trash area, a dedicated space for deleted content. This allows you to recover mistakenly deleted content or permanently remove items when you're sure you don't need them anymore.</p>
|
|
||||||
<p>The trash applies to both files and projects. Items in the trash remain there for a certain period depending on your Penpot subscription plan before being automatically deleted permanently.</p>
|
|
||||||
|
|
||||||
<h3 id="access-trash">Access the trash</h3>
|
|
||||||
<p>A <strong>Trash</strong> section is accessible from the dashboard navigation. When you access it, you'll see all your deleted files and projects, each clearly labeled so you can easily identify what you want to restore or permanently delete.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/files-projects/03-trash.webp" alt="Trash area" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<h3 id="trash-permissions">Trash permissions</h3>
|
|
||||||
<p>Access to the trash and the actions you can perform depend on your role in the team:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Owner, Admin, and Editor:</strong> Can view the trash, restore deleted items, and permanently delete items from the trash.</li>
|
|
||||||
<li><strong>Viewer:</strong> Cannot access the trash or manage deleted content.</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h3 id="restore-items">Restore items</h3>
|
|
||||||
<p>To restore a deleted file or project, access the trash area and find the item you want to recover. Select the item and choose <strong>Restore</strong>. The item will be restored to its original location (the project it belonged to, or the drafts section if it wasn't in a project).</p>
|
|
||||||
|
|
||||||
<h3 id="permanently-delete">Permanently delete items</h3>
|
|
||||||
<p>If you're sure you don't need an item anymore, you can permanently delete it from the trash. Select the item and choose <strong>Permanently delete</strong>. This action cannot be undone, so make sure you really want to remove the item permanently.</p>
|
|
||||||
<p class="advice">Items in the trash are automatically deleted after a certain period depending on your subscription plan. If you want to keep something, restore it before the auto-deletion period expires.</p>
|
|
||||||
@@ -455,43 +455,6 @@ ExtraBold Italic
|
|||||||
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
<p>A <strong>Typography composite token</strong> can be applied to a full text layer to set all typography properties at once. This lets you manage complete text styles using a single token instead of combining multiple individual ones.</p>
|
||||||
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
<p>When applying a Typography composite token to a layer, any previously applied <em>Typography composite token</em> or <em>style</em> will be detached. The same happens in reverse. Only one of them can be active at a time.</p>
|
||||||
|
|
||||||
<h3 id="design-tokens-shadow">Shadow</h3>
|
|
||||||
<p>Shadow tokens are composite entities that encapsulate the properties of one or more shadows into a single token definition. This token can contain a single shadow or an array of multiple shadows that can be reordered.</p>
|
|
||||||
<p>Shadow tokens support both <strong>Drop Shadow</strong> and <strong>Inner Shadow</strong> types. When creating or editing a shadow token, you can select the type of shadow you want to use. The default selection is Drop Shadow.</p>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/design-tokens/37-tokens-shadow-individual.webp" alt="Shadow token creation with individual values" />
|
|
||||||
</figure>
|
|
||||||
|
|
||||||
<h4 id="design-tokens-shadow-properties">Shadow properties</h4>
|
|
||||||
<p>Each shadow within a shadow token contains a set of properties that define how the shadow appears:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Color:</strong> The color of the shadow. Accepts the same values as <a href="#design-tokens-color">color tokens</a> (Hex, RGB, RGBA, ARGB, HSL, HSLA), and you can reference existing color tokens. The color picker is available when defining the value.</li>
|
|
||||||
<li><strong>X offset:</strong> The horizontal offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
|
||||||
<li><strong>Y offset:</strong> The vertical offset of the shadow. Can be unit or unitless, and accepts negative values. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
|
||||||
<li><strong>Blur:</strong> The blur radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
|
||||||
<li><strong>Spread:</strong> The spread radius of the shadow. Can be unit or unitless. You can use a number or reference a <a href="#design-tokens-number">number</a> or <a href="#design-tokens-dimensions">dimension</a> token.</li>
|
|
||||||
<li><strong>Type:</strong> Whether the shadow is a drop shadow or an inner shadow. Selected via a dropdown menu, with Drop Shadow as the default.</li>
|
|
||||||
</ul>
|
|
||||||
<p>Each property within a shadow token can reference existing tokens or be assigned hardcoded values. Shadows can also reference other shadow tokens (the type of shadow must match when using references).</p>
|
|
||||||
<p class="advice">Not all properties are mandatory to save a shadow token. Some can be empty (and will be computed as 0). Only the color property is mandatory. In an array of shadows, if any shadow does not have the color set, the form cannot be saved.</p>
|
|
||||||
|
|
||||||
<h4 id="design-tokens-shadow-create">Creating shadow tokens</h4>
|
|
||||||
<p>To create a shadow token, click on the <strong>+</strong> next to <strong>Shadow</strong> in the Tokens panel. Shadow tokens can be created in two ways:</p>
|
|
||||||
<ul>
|
|
||||||
<li><strong>Individual values:</strong> You can create one shadow or multiple shadows with individual property values. Click the <strong>+</strong> button to add more shadows to the array. New shadows are added at the top of the list.</li>
|
|
||||||
<li><strong>Single reference:</strong> You can reference another existing shadow token. When using a single reference, you cannot add more than one shadow. The resolved value will display the shadow or list of shadows that the referenced token contains.</li>
|
|
||||||
</ul>
|
|
||||||
<figure>
|
|
||||||
<img src="/img/design-tokens/38-tokens-shadow-reference.webp" alt="Shadow token creation with reference" />
|
|
||||||
</figure>
|
|
||||||
<p>When creating a shadow with individual values, the color value starts empty, but the other inputs have default values (X: 4, Y: 4, Blur: 4, Spread: 0). You can reorder shadows by hovering over a shadow form and using the reorder button to drag it to a different position.</p>
|
|
||||||
<p>You can also reference another existing shadow token instead of defining each property manually. When doing so, Penpot resolves all shadow properties from the referenced token.</p>
|
|
||||||
|
|
||||||
<h4 id="design-tokens-shadow-apply">Applying shadow tokens</h4>
|
|
||||||
<p>Shadow tokens can be applied to any layer type. Clicking on a shadow token will apply it to the selected layer. Right-clicking on a shadow token shows the context menu with the <strong>Shadow</strong> option to apply it.</p>
|
|
||||||
<p class="advice">Text elements in CSS do not support inner shadows, but Penpot does, since it uses the filter property internally instead of the box-shadow property.</p>
|
|
||||||
<p>When applying a shadow token, any existing shadow on the layer will be overridden (whether it's a raw shadow or an applied token shadow). If the token contains an array of shadows, each shadow will be added in the same order as in the creation form.</p>
|
|
||||||
<p class="advice">In Penpot, an element can have multiple shadows, but only one token of the same type can be applied. This means that applying a second shadow token would override the first one, regardless of how many shadows the shape currently has.</p>
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -101,6 +101,70 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(brTokenPillXL).not.toBeVisible();
|
await expect(brTokenPillXL).not.toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User applies opacity token to a shape from sidebar", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||||
|
await setupTokensFile(page);
|
||||||
|
|
||||||
|
await page.getByRole("tab", { name: "Layers" }).click();
|
||||||
|
|
||||||
|
await workspacePage.layers.getByTestId("layer-row").nth(1).click();
|
||||||
|
|
||||||
|
// Open tokens sections on left sidebar
|
||||||
|
const tokensTabButton = page.getByRole("tab", { name: "Tokens" });
|
||||||
|
await tokensTabButton.click();
|
||||||
|
|
||||||
|
// Unfold opacity tokens
|
||||||
|
await page.getByRole("button", { name: "Opacity 3" }).click();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "opacity", exact: true }),
|
||||||
|
).toBeVisible();
|
||||||
|
await tokensSidebar
|
||||||
|
.getByRole("button", { name: "opacity", exact: true })
|
||||||
|
.click();
|
||||||
|
await expect(
|
||||||
|
tokensSidebar.getByRole("button", { name: "opacity.high" }),
|
||||||
|
).toBeVisible();
|
||||||
|
|
||||||
|
// Apply opacity token from token panels
|
||||||
|
await tokensSidebar.getByRole("button", { name: "opacity.high" }).click();
|
||||||
|
|
||||||
|
// Check if opacity sections is visible on right sidebar
|
||||||
|
const layerMenuSection = page.getByRole("region", {
|
||||||
|
name: "layer-menu-section",
|
||||||
|
});
|
||||||
|
await expect(layerMenuSection).toBeVisible();
|
||||||
|
|
||||||
|
// Check if token pill is visible on design tab on right sidebar
|
||||||
|
const opacityHighPill = layerMenuSection.getByRole("button", {
|
||||||
|
name: "opacity.high",
|
||||||
|
});
|
||||||
|
await expect(opacityHighPill).toBeVisible();
|
||||||
|
|
||||||
|
// Detach token from design tab on right sidebar
|
||||||
|
const detachButton = layerMenuSection.getByRole("button", {
|
||||||
|
name: "Detach token",
|
||||||
|
});
|
||||||
|
await detachButton.click();
|
||||||
|
|
||||||
|
// Open dropdown from input
|
||||||
|
const dropdownBtn = layerMenuSection.getByLabel('Open token list');
|
||||||
|
await expect(dropdownBtn).toBeVisible();
|
||||||
|
await dropdownBtn.click();
|
||||||
|
|
||||||
|
// Change token from dropdown
|
||||||
|
const opacityLowOption = layerMenuSection.getByRole('option', { name: 'opacity.low' });
|
||||||
|
await expect(opacityLowOption).toBeVisible();
|
||||||
|
await opacityLowOption.click();
|
||||||
|
|
||||||
|
await expect(opacityHighPill).not.toBeVisible();
|
||||||
|
const opacityLowPill = layerMenuSection.getByRole("button", {
|
||||||
|
name: "opacity.low",
|
||||||
|
});
|
||||||
|
await expect(opacityLowPill).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("User applies typography token to a text shape", async ({ page }) => {
|
test("User applies typography token to a text shape", async ({ page }) => {
|
||||||
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
const { workspacePage, tokensSidebar, tokenContextMenuForToken } =
|
||||||
await setupTypographyTokensFile(page);
|
await setupTypographyTokensFile(page);
|
||||||
@@ -129,189 +193,6 @@ test.describe("Tokens: Apply token", () => {
|
|||||||
await expect(fontSizeInput).toHaveValue("100");
|
await expect(fontSizeInput).toHaveValue("100");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("User edits typography token and all fields are valid", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
await tokensSidebar
|
|
||||||
.getByRole("button")
|
|
||||||
.filter({ hasText: "Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
// Open edit modal for "Full" typography token
|
|
||||||
const token = tokensSidebar.getByRole("button", { name: "Full" });
|
|
||||||
await token.click({ button: "right" });
|
|
||||||
await page.getByText("Edit token").click();
|
|
||||||
|
|
||||||
// Modal opens
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const saveButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: /save/i,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Fill font-family to verify to verify that input value doesn't get split into list of characters
|
|
||||||
const fontFamilyField = tokensUpdateCreateModal
|
|
||||||
.getByLabel("Font family")
|
|
||||||
.first();
|
|
||||||
await fontFamilyField.fill("OneWord");
|
|
||||||
|
|
||||||
// Invalidate incorrect values for font size
|
|
||||||
const fontSizeField = tokensUpdateCreateModal.getByLabel(/Font Size/i);
|
|
||||||
await fontSizeField.fill("invalid");
|
|
||||||
await expect(
|
|
||||||
tokensUpdateCreateModal.getByText(/Invalid token value:/),
|
|
||||||
).toBeVisible();
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Show error with line-height depending on invalid font-size
|
|
||||||
await fontSizeField.fill("");
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Fill in values for all fields and verify they persist when switching tabs
|
|
||||||
await fontSizeField.fill("16");
|
|
||||||
await expect(saveButton).toBeEnabled();
|
|
||||||
|
|
||||||
const fontWeightField = tokensUpdateCreateModal.getByLabel(/Font Weight/i);
|
|
||||||
const letterSpacingField =
|
|
||||||
tokensUpdateCreateModal.getByLabel(/Letter Spacing/i);
|
|
||||||
const lineHeightField = tokensUpdateCreateModal.getByLabel(/Line Height/i);
|
|
||||||
const textCaseField = tokensUpdateCreateModal.getByLabel(/Text Case/i);
|
|
||||||
const textDecorationField =
|
|
||||||
tokensUpdateCreateModal.getByLabel(/Text Decoration/i);
|
|
||||||
|
|
||||||
// Capture all values before switching tabs
|
|
||||||
const originalValues = {
|
|
||||||
fontSize: await fontSizeField.inputValue(),
|
|
||||||
fontFamily: await fontFamilyField.inputValue(),
|
|
||||||
fontWeight: await fontWeightField.inputValue(),
|
|
||||||
letterSpacing: await letterSpacingField.inputValue(),
|
|
||||||
lineHeight: await lineHeightField.inputValue(),
|
|
||||||
textCase: await textCaseField.inputValue(),
|
|
||||||
textDecoration: await textDecorationField.inputValue(),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Switch to reference tab and back to composite tab
|
|
||||||
const referenceTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceTabButton.click();
|
|
||||||
|
|
||||||
// Empty reference tab should be disabled
|
|
||||||
await expect(saveButton).toBeDisabled();
|
|
||||||
|
|
||||||
const compositeTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("composite-opt");
|
|
||||||
await compositeTabButton.click();
|
|
||||||
|
|
||||||
// Filled composite tab should be enabled
|
|
||||||
await expect(saveButton).toBeEnabled();
|
|
||||||
|
|
||||||
// Verify all values are preserved after switching tabs
|
|
||||||
await expect(fontSizeField).toHaveValue(originalValues.fontSize);
|
|
||||||
await expect(fontFamilyField).toHaveValue(originalValues.fontFamily);
|
|
||||||
await expect(fontWeightField).toHaveValue(originalValues.fontWeight);
|
|
||||||
await expect(letterSpacingField).toHaveValue(originalValues.letterSpacing);
|
|
||||||
await expect(lineHeightField).toHaveValue(originalValues.lineHeight);
|
|
||||||
await expect(textCaseField).toHaveValue(originalValues.textCase);
|
|
||||||
await expect(textDecorationField).toHaveValue(
|
|
||||||
originalValues.textDecoration,
|
|
||||||
);
|
|
||||||
|
|
||||||
await saveButton.click();
|
|
||||||
|
|
||||||
// Modal should close, token should be visible (with new name) in sidebar
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User cant submit empty typography token or reference", async ({
|
|
||||||
page,
|
|
||||||
}) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill("typography.empty");
|
|
||||||
|
|
||||||
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
|
|
||||||
|
|
||||||
// Insert a value and then delete it
|
|
||||||
await valueField.fill("1");
|
|
||||||
await valueField.fill("");
|
|
||||||
|
|
||||||
// Submit button should be disabled when field is empty
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
|
|
||||||
// Switch to reference tab, should not be submittable either
|
|
||||||
const referenceTabButton =
|
|
||||||
tokensUpdateCreateModal.getByTestId("reference-opt");
|
|
||||||
await referenceTabButton.click();
|
|
||||||
await expect(submitButton).toBeDisabled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User adds typography token with reference", async ({ page }) => {
|
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
|
||||||
await setupTypographyTokensFile(page);
|
|
||||||
|
|
||||||
const newTokenTitle = "NewReference";
|
|
||||||
|
|
||||||
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
|
||||||
await tokensTabPanel
|
|
||||||
.getByRole("button", { name: "Add Token: Typography" })
|
|
||||||
.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).toBeVisible();
|
|
||||||
|
|
||||||
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
|
||||||
await nameField.fill(newTokenTitle);
|
|
||||||
|
|
||||||
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Use a reference",
|
|
||||||
});
|
|
||||||
referenceTabButton.click();
|
|
||||||
|
|
||||||
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
|
||||||
name: "Reference",
|
|
||||||
});
|
|
||||||
await referenceField.fill("{Full}");
|
|
||||||
|
|
||||||
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
|
||||||
name: "Save",
|
|
||||||
});
|
|
||||||
|
|
||||||
const resolvedValue =
|
|
||||||
await tokensUpdateCreateModal.getByText("Resolved value:");
|
|
||||||
await expect(resolvedValue).toBeVisible();
|
|
||||||
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
|
|
||||||
await expect(resolvedValue).toContainText("Font Size: 100");
|
|
||||||
await expect(resolvedValue).toContainText("Font Weight: 300");
|
|
||||||
await expect(resolvedValue).toContainText("Letter Spacing: 2");
|
|
||||||
await expect(resolvedValue).toContainText("Text Case: uppercase");
|
|
||||||
await expect(resolvedValue).toContainText("Text Decoration: underline");
|
|
||||||
|
|
||||||
await expect(submitButton).toBeEnabled();
|
|
||||||
await submitButton.click();
|
|
||||||
|
|
||||||
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
|
||||||
|
|
||||||
const newToken = tokensSidebar.getByRole("button", {
|
|
||||||
name: newTokenTitle,
|
|
||||||
});
|
|
||||||
|
|
||||||
await expect(newToken).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("User adds shadow token with multiple shadows and applies it to shape", async ({
|
test("User adds shadow token with multiple shadows and applies it to shape", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
@@ -1008,6 +1008,41 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User cant submit empty typography token or reference", async ({
|
||||||
|
page,
|
||||||
|
}) => {
|
||||||
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill("typography.empty");
|
||||||
|
|
||||||
|
const valueField = tokensUpdateCreateModal.getByLabel("Font Size");
|
||||||
|
|
||||||
|
// Insert a value and then delete it
|
||||||
|
await valueField.fill("1");
|
||||||
|
await valueField.fill("");
|
||||||
|
|
||||||
|
// Submit button should be disabled when field is empty
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
|
||||||
|
// Switch to reference tab, should not be submittable either
|
||||||
|
const referenceTabButton =
|
||||||
|
tokensUpdateCreateModal.getByTestId("reference-opt");
|
||||||
|
await referenceTabButton.click();
|
||||||
|
await expect(submitButton).toBeDisabled();
|
||||||
|
});
|
||||||
|
|
||||||
test("User creates typography token", async ({ page }) => {
|
test("User creates typography token", async ({ page }) => {
|
||||||
const emptyNameError = "Name should be at least 1 character";
|
const emptyNameError = "Name should be at least 1 character";
|
||||||
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar } =
|
||||||
@@ -1256,6 +1291,58 @@ test.describe("Tokens - CRUD", () => {
|
|||||||
).toBeEnabled();
|
).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("User adds typography token with reference", async ({ page }) => {
|
||||||
|
const { tokensUpdateCreateModal, tokenThemesSetsSidebar, tokensSidebar } =
|
||||||
|
await setupTypographyTokensFile(page);
|
||||||
|
|
||||||
|
const newTokenTitle = "NewReference";
|
||||||
|
|
||||||
|
const tokensTabPanel = page.getByRole("tabpanel", { name: "tokens" });
|
||||||
|
await tokensTabPanel
|
||||||
|
.getByRole("button", { name: "Add Token: Typography" })
|
||||||
|
.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).toBeVisible();
|
||||||
|
|
||||||
|
const nameField = tokensUpdateCreateModal.getByLabel("Name");
|
||||||
|
await nameField.fill(newTokenTitle);
|
||||||
|
|
||||||
|
const referenceTabButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Use a reference",
|
||||||
|
});
|
||||||
|
referenceTabButton.click();
|
||||||
|
|
||||||
|
const referenceField = tokensUpdateCreateModal.getByRole("textbox", {
|
||||||
|
name: "Reference",
|
||||||
|
});
|
||||||
|
await referenceField.fill("{Full}");
|
||||||
|
|
||||||
|
const submitButton = tokensUpdateCreateModal.getByRole("button", {
|
||||||
|
name: "Save",
|
||||||
|
});
|
||||||
|
|
||||||
|
const resolvedValue =
|
||||||
|
await tokensUpdateCreateModal.getByText("Resolved value:");
|
||||||
|
await expect(resolvedValue).toBeVisible();
|
||||||
|
await expect(resolvedValue).toContainText("Font Family: 42dot Sans");
|
||||||
|
await expect(resolvedValue).toContainText("Font Size: 100");
|
||||||
|
await expect(resolvedValue).toContainText("Font Weight: 300");
|
||||||
|
await expect(resolvedValue).toContainText("Letter Spacing: 2");
|
||||||
|
await expect(resolvedValue).toContainText("Text Case: uppercase");
|
||||||
|
await expect(resolvedValue).toContainText("Text Decoration: underline");
|
||||||
|
|
||||||
|
await expect(submitButton).toBeEnabled();
|
||||||
|
await submitButton.click();
|
||||||
|
|
||||||
|
await expect(tokensUpdateCreateModal).not.toBeVisible();
|
||||||
|
|
||||||
|
const newToken = tokensSidebar.getByRole("button", {
|
||||||
|
name: newTokenTitle,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(newToken).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
test("User edits typography token and all fields are valid", async ({
|
test("User edits typography token and all fields are valid", async ({
|
||||||
page,
|
page,
|
||||||
}) => {
|
}) => {
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 138 KiB After Width: | Height: | Size: 279 KiB |
@@ -61,11 +61,6 @@
|
|||||||
;; Def micro-benchmark iterations
|
;; Def micro-benchmark iterations
|
||||||
(def micro-benchmark-iterations 1e6)
|
(def micro-benchmark-iterations 1e6)
|
||||||
|
|
||||||
;; Performance logs
|
|
||||||
(defonce ^:private longtask-observer* (atom nil))
|
|
||||||
(defonce ^:private stall-timer* (atom nil))
|
|
||||||
(defonce ^:private current-op* (atom nil))
|
|
||||||
|
|
||||||
;; --- CONTEXT
|
;; --- CONTEXT
|
||||||
|
|
||||||
(defn- collect-context
|
(defn- collect-context
|
||||||
@@ -469,72 +464,3 @@
|
|||||||
(defn event
|
(defn event
|
||||||
[props]
|
[props]
|
||||||
(ptk/data-event ::event props))
|
(ptk/data-event ::event props))
|
||||||
|
|
||||||
;; --- DEVTOOLS PERF LOGGING
|
|
||||||
|
|
||||||
(defn install-long-task-observer! []
|
|
||||||
(when (and (some? (.-PerformanceObserver js/window)) (nil? @longtask-observer*))
|
|
||||||
(let [observer (js/PerformanceObserver.
|
|
||||||
(fn [list _]
|
|
||||||
(doseq [entry (.getEntries list)]
|
|
||||||
(let [dur (.-duration entry)
|
|
||||||
start (.-startTime entry)
|
|
||||||
attrib (.-attribution entry)
|
|
||||||
attrib-count (when attrib (.-length attrib))
|
|
||||||
first-attrib (when (and attrib-count (> attrib-count 0)) (aget attrib 0))
|
|
||||||
attrib-name (when first-attrib (.-name first-attrib))
|
|
||||||
attrib-ctype (when first-attrib (.-containerType first-attrib))
|
|
||||||
attrib-cid (when first-attrib (.-containerId first-attrib))
|
|
||||||
attrib-csrc (when first-attrib (.-containerSrc first-attrib))]
|
|
||||||
|
|
||||||
(.warn js/console (str "[perf] long task " (Math/round dur) "ms at " (Math/round start) "ms"
|
|
||||||
(when first-attrib
|
|
||||||
(str " attrib:name=" attrib-name
|
|
||||||
" ctype=" attrib-ctype
|
|
||||||
" cid=" attrib-cid
|
|
||||||
" csrc=" attrib-csrc))))))))]
|
|
||||||
(.observe observer #js{:entryTypes #js["longtask"]})
|
|
||||||
(reset! longtask-observer* observer))))
|
|
||||||
|
|
||||||
(defn start-event-loop-stall-logger!
|
|
||||||
"Log event loop stalls by measuring setInterval drift.
|
|
||||||
interval-ms: base interval
|
|
||||||
threshold-ms: drift over which we report"
|
|
||||||
[interval-ms threshold-ms]
|
|
||||||
(when (nil? @stall-timer*)
|
|
||||||
(let [last (atom (.now js/performance))
|
|
||||||
id (js/setInterval
|
|
||||||
(fn []
|
|
||||||
(let [now (.now js/performance)
|
|
||||||
expected (+ @last interval-ms)
|
|
||||||
drift (- now expected)
|
|
||||||
current-op @current-op*
|
|
||||||
measures (.getEntriesByType js/performance "measure")
|
|
||||||
mlen (.-length measures)
|
|
||||||
last-measure (when (> mlen 0) (aget measures (dec mlen)))
|
|
||||||
meas-name (when last-measure (.-name last-measure))
|
|
||||||
meas-detail (when last-measure (.-detail last-measure))
|
|
||||||
meas-count (when meas-detail (unchecked-get meas-detail "count"))]
|
|
||||||
(reset! last now)
|
|
||||||
(when (> drift threshold-ms)
|
|
||||||
(.warn js/console
|
|
||||||
(str "[perf] event loop stall: " (Math/round drift) "ms"
|
|
||||||
(when current-op (str " op=" current-op))
|
|
||||||
(when meas-name (str " last=" meas-name))
|
|
||||||
(when meas-count (str " count=" meas-count)))))))
|
|
||||||
interval-ms)]
|
|
||||||
(reset! stall-timer* id))))
|
|
||||||
|
|
||||||
(defn init!
|
|
||||||
"Install perf observers in dev builds. Safe to call multiple times."
|
|
||||||
[]
|
|
||||||
(when ^boolean js/goog.DEBUG
|
|
||||||
(install-long-task-observer!)
|
|
||||||
(start-event-loop-stall-logger! 50 100)
|
|
||||||
;; Expose simple API on window for manual control in devtools
|
|
||||||
(let [api #js {:reset (fn []
|
|
||||||
(try
|
|
||||||
(.clearMarks js/performance)
|
|
||||||
(.clearMeasures js/performance)
|
|
||||||
(catch :default _ nil)))}]
|
|
||||||
(aset js/window "PenpotPerf" api))))
|
|
||||||
|
|||||||
@@ -173,8 +173,7 @@
|
|||||||
params (assoc params :team-id team-id)]
|
params (assoc params :team-id team-id)]
|
||||||
(->> (rp/cmd! :delete-team-member params)
|
(->> (rp/cmd! :delete-team-member params)
|
||||||
(rx/mapcat (fn [_]
|
(rx/mapcat (fn [_]
|
||||||
(rx/of (dp/refresh-profile)
|
(rx/of (fetch-members team-id)
|
||||||
(fetch-members team-id)
|
|
||||||
(fetch-teams)
|
(fetch-teams)
|
||||||
(ptk/data-event ::ev/event
|
(ptk/data-event ::ev/event
|
||||||
{::ev/name "delete-team-member"
|
{::ev/name "delete-team-member"
|
||||||
|
|||||||
@@ -347,12 +347,6 @@
|
|||||||
(with-meta {:team-id team-id
|
(with-meta {:team-id team-id
|
||||||
:file-id file-id}))))))
|
:file-id file-id}))))))
|
||||||
|
|
||||||
;; Install dev perf observers once the workspace is ready
|
|
||||||
(->> stream
|
|
||||||
(rx/filter (ptk/type? ::workspace-initialized))
|
|
||||||
(rx/take 1)
|
|
||||||
(rx/map (fn [_] (ev/init!))))
|
|
||||||
|
|
||||||
(->> stream
|
(->> stream
|
||||||
(rx/filter (ptk/type? ::dps/persistence-notification))
|
(rx/filter (ptk/type? ::dps/persistence-notification))
|
||||||
(rx/take 1)
|
(rx/take 1)
|
||||||
|
|||||||
@@ -18,13 +18,13 @@
|
|||||||
ptk/UpdateEvent
|
ptk/UpdateEvent
|
||||||
(update [_ state]
|
(update [_ state]
|
||||||
(let [expand-fn (fn [expanded]
|
(let [expand-fn (fn [expanded]
|
||||||
(let [parents-seqs (map (fn [x] (cfh/get-parent-ids objects x)) ids)
|
(merge expanded
|
||||||
flat-parents (apply concat parents-seqs)
|
(->> ids
|
||||||
non-root-parents (remove #(= % uuid/zero) flat-parents)
|
(map #(cfh/get-parent-ids objects %))
|
||||||
distinct-parents (into #{} non-root-parents)]
|
flatten
|
||||||
(merge expanded
|
(remove #(= % uuid/zero))
|
||||||
(into {}
|
(map (fn [id] {id true}))
|
||||||
(map (fn [id] {id true}) distinct-parents)))))]
|
(into {}))))]
|
||||||
(update-in state [:workspace-local :expanded] expand-fn)))))
|
(update-in state [:workspace-local :expanded] expand-fn)))))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -264,13 +264,10 @@
|
|||||||
|
|
||||||
ptk/WatchEvent
|
ptk/WatchEvent
|
||||||
(watch [_ state _]
|
(watch [_ state _]
|
||||||
(let [objects (dsh/lookup-page-objects state)
|
(let [objects (dsh/lookup-page-objects state)]
|
||||||
;; Schedule expanding parents asynchronously to avoid blocking
|
(rx/of
|
||||||
;; the event loop
|
(dwc/expand-all-parents ids objects)
|
||||||
expand-s (->> (rx/of (dwc/expand-all-parents ids objects))
|
::dwsp/interrupt)))))
|
||||||
(rx/observe-on :async))
|
|
||||||
interrupt-s (rx/of ::dwsp/interrupt)]
|
|
||||||
(rx/merge expand-s interrupt-s)))))
|
|
||||||
|
|
||||||
(defn select-all
|
(defn select-all
|
||||||
[]
|
[]
|
||||||
|
|||||||
@@ -305,7 +305,7 @@
|
|||||||
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
|
(l/derived #(dsh/lookup-shape % page-id shape-id) st/state =))
|
||||||
|
|
||||||
(def workspace-page-objects
|
(def workspace-page-objects
|
||||||
(l/derived dsh/lookup-page-objects st/state identical?))
|
(l/derived dsh/lookup-page-objects st/state))
|
||||||
|
|
||||||
(def workspace-read-only?
|
(def workspace-read-only?
|
||||||
(l/derived :read-only? workspace-global))
|
(l/derived :read-only? workspace-global))
|
||||||
|
|||||||
@@ -216,7 +216,7 @@
|
|||||||
is-selected-on-focus nillable
|
is-selected-on-focus nillable
|
||||||
tokens applied-token empty-to-end
|
tokens applied-token empty-to-end
|
||||||
on-change on-blur on-focus on-detach
|
on-change on-blur on-focus on-detach
|
||||||
property align ref]
|
property align ref name]
|
||||||
:rest props}]
|
:rest props}]
|
||||||
|
|
||||||
(let [;; NOTE: we use mfu/bean here for transparently handle
|
(let [;; NOTE: we use mfu/bean here for transparently handle
|
||||||
@@ -662,7 +662,10 @@
|
|||||||
label (get token :name)
|
label (get token :name)
|
||||||
token-value (or (get token :resolved-value)
|
token-value (or (get token :resolved-value)
|
||||||
(or (mf/ref-val last-value*)
|
(or (mf/ref-val last-value*)
|
||||||
(fmt/format-number value)))]
|
(fmt/format-number value)))
|
||||||
|
token-value (if (= name :opacity)
|
||||||
|
(* 100 token-value)
|
||||||
|
token-value)]
|
||||||
(mf/spread-props props
|
(mf/spread-props props
|
||||||
{:id id
|
{:id id
|
||||||
:label label
|
:label label
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
@use "ds/_borders.scss" as *;
|
@use "ds/_borders.scss" as *;
|
||||||
@use "ds/_sizes.scss" as *;
|
@use "ds/_sizes.scss" as *;
|
||||||
@use "ds/typography.scss" as *;
|
@use "ds/typography.scss" as *;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
.option-list {
|
.option-list {
|
||||||
--options-dropdown-icon-fg-color: var(--color-foreground-secondary);
|
--options-dropdown-icon-fg-color: var(--color-foreground-secondary);
|
||||||
@@ -15,32 +16,32 @@
|
|||||||
--options-dropdown-border-color: var(--color-background-quaternary);
|
--options-dropdown-border-color: var(--color-background-quaternary);
|
||||||
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: $sz-36;
|
inset-block-start: $sz-36;
|
||||||
width: var(--dropdown-width, 100%);
|
inline-size: var(--dropdown-width, 100%);
|
||||||
transform: translateX(var(--dropdown-translate-distance, 0));
|
transform: translateX(var(--dropdown-translate-distance, 0));
|
||||||
background-color: var(--options-dropdown-bg-color);
|
background-color: var(--options-dropdown-bg-color);
|
||||||
border-radius: $br-8;
|
border-radius: $br-8;
|
||||||
border: $b-1 solid var(--options-dropdown-border-color);
|
border: $b-1 solid var(--options-dropdown-border-color);
|
||||||
padding-block: var(--sp-xs);
|
padding-block: var(--sp-xs);
|
||||||
margin-block-end: 0;
|
margin-block-end: 0;
|
||||||
max-height: $sz-400;
|
max-block-size: $sz-400;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
z-index: var(--z-index-dropdown);
|
z-index: var(--z-index-dropdown);
|
||||||
}
|
}
|
||||||
|
|
||||||
.left-align {
|
.left-align {
|
||||||
left: var(--dropdown-offset, 0);
|
inset-inline-start: var(--dropdown-offset, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.right-align {
|
.right-align {
|
||||||
right: var(--dropdown-offset, 0);
|
inset-inline-end: var(--dropdown-offset, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-separator {
|
.option-separator {
|
||||||
border: $b-1 solid var(--options-dropdown-border-color);
|
border: $b-1 solid var(--options-dropdown-border-color);
|
||||||
margin-top: var(--sp-xs);
|
margin-block-start: var(--sp-xs);
|
||||||
margin-bottom: var(--sp-xs);
|
margin-block-end: var(--sp-xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.group-option,
|
.group-option,
|
||||||
@@ -51,11 +52,11 @@
|
|||||||
gap: var(--sp-xs);
|
gap: var(--sp-xs);
|
||||||
color: var(--color-foreground-secondary);
|
color: var(--color-foreground-secondary);
|
||||||
padding-inline: var(--sp-s);
|
padding-inline: var(--sp-s);
|
||||||
height: var(--sp-xxxl);
|
block-size: var(--sp-xxxl);
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-empty {
|
.option-empty {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0 40px;
|
padding: 0 px2rem(40);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,24 +33,9 @@
|
|||||||
[okulary.core :as l]
|
[okulary.core :as l]
|
||||||
[rumext.v2 :as mf]))
|
[rumext.v2 :as mf]))
|
||||||
|
|
||||||
;; Coalesce sidebar hover highlights to 1 frame to avoid long tasks
|
|
||||||
(defonce ^:private sidebar-hover-queue (atom {:enter #{} :leave #{}}))
|
|
||||||
(defonce ^:private sidebar-hover-pending? (atom false))
|
|
||||||
|
|
||||||
(defn- schedule-sidebar-hover-flush []
|
|
||||||
(when (compare-and-set! sidebar-hover-pending? false true)
|
|
||||||
(ts/raf
|
|
||||||
(fn []
|
|
||||||
(let [{:keys [enter leave]} (swap! sidebar-hover-queue (constantly {:enter #{} :leave #{}}))]
|
|
||||||
(reset! sidebar-hover-pending? false)
|
|
||||||
(when (seq leave)
|
|
||||||
(apply st/emit! (map dw/dehighlight-shape leave)))
|
|
||||||
(when (seq enter)
|
|
||||||
(apply st/emit! (map dw/highlight-shape enter))))))))
|
|
||||||
|
|
||||||
(mf/defc layer-item-inner
|
(mf/defc layer-item-inner
|
||||||
{::mf/wrap-props false}
|
{::mf/wrap-props false}
|
||||||
[{:keys [item depth parent-size name-ref children ref style
|
[{:keys [item depth parent-size name-ref children ref
|
||||||
;; Flags
|
;; Flags
|
||||||
read-only? highlighted? selected? component-tree?
|
read-only? highlighted? selected? component-tree?
|
||||||
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
filtered? expanded? dnd-over? dnd-over-top? dnd-over-bot? hide-toggle?
|
||||||
@@ -97,8 +82,7 @@
|
|||||||
:dnd-over dnd-over?
|
:dnd-over dnd-over?
|
||||||
:dnd-over-top dnd-over-top?
|
:dnd-over-top dnd-over-top?
|
||||||
:dnd-over-bot dnd-over-bot?
|
:dnd-over-bot dnd-over-bot?
|
||||||
:root-board parent-board?)
|
:root-board parent-board?)}
|
||||||
:style style}
|
|
||||||
[:span {:class (stl/css-case
|
[:span {:class (stl/css-case
|
||||||
:tab-indentation true
|
:tab-indentation true
|
||||||
:filtered filtered?)
|
:filtered filtered?)
|
||||||
@@ -181,12 +165,10 @@
|
|||||||
|
|
||||||
children]))
|
children]))
|
||||||
|
|
||||||
;; Memoized for performance
|
|
||||||
(mf/defc layer-item
|
(mf/defc layer-item
|
||||||
{::mf/props :obj
|
{::mf/props :obj
|
||||||
::mf/wrap [mf/memo]}
|
::mf/memo true}
|
||||||
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted style render-children?]
|
[{:keys [index item selected objects sortable? filtered? depth parent-size component-child? highlighted]}]
|
||||||
:or {render-children? true}}]
|
|
||||||
(let [id (:id item)
|
(let [id (:id item)
|
||||||
blocked? (:blocked item)
|
blocked? (:blocked item)
|
||||||
hidden? (:hidden item)
|
hidden? (:hidden item)
|
||||||
@@ -263,21 +245,13 @@
|
|||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id)
|
(mf/deps id)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
(st/emit! (dw/highlight-shape id))))
|
||||||
(-> q
|
|
||||||
(assoc :enter (conj enter id))
|
|
||||||
(assoc :leave (disj leave id)))))
|
|
||||||
(schedule-sidebar-hover-flush)))
|
|
||||||
|
|
||||||
on-pointer-leave
|
on-pointer-leave
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps id)
|
(mf/deps id)
|
||||||
(fn [_]
|
(fn [_]
|
||||||
(swap! sidebar-hover-queue (fn [{:keys [enter leave] :as q}]
|
(st/emit! (dw/dehighlight-shape id))))
|
||||||
(-> q
|
|
||||||
(assoc :enter (disj enter id))
|
|
||||||
(assoc :leave (conj leave id)))))
|
|
||||||
(schedule-sidebar-hover-flush)))
|
|
||||||
|
|
||||||
on-context-menu
|
on-context-menu
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@@ -363,18 +337,14 @@
|
|||||||
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
component-tree? (or component-child? (ctk/instance-root? item) (ctk/instance-head? item))
|
||||||
|
|
||||||
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
enable-drag (mf/use-fn #(reset! drag-disabled* false))
|
||||||
disable-drag (mf/use-fn #(reset! drag-disabled* true))
|
disable-drag (mf/use-fn #(reset! drag-disabled* true))]
|
||||||
|
|
||||||
;; Lazy loading of child elements via IntersectionObserver
|
|
||||||
children-count* (mf/use-state 0)
|
|
||||||
children-count (deref children-count*)
|
|
||||||
lazy-ref (mf/use-ref nil)
|
|
||||||
observer-var (mf/use-var nil)
|
|
||||||
chunk-size 50]
|
|
||||||
|
|
||||||
(mf/with-effect [selected? selected]
|
(mf/with-effect [selected? selected]
|
||||||
(let [single? (= (count selected) 1)
|
(let [single? (= (count selected) 1)
|
||||||
node (mf/ref-val ref)
|
node (mf/ref-val ref)
|
||||||
|
;; NOTE: Neither get-parent-at nor get-parent-with-selector
|
||||||
|
;; work if the component template changes, so we need to
|
||||||
|
;; seek for an alternate solution. Maybe use-context?
|
||||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
scroll-node (dom/get-parent-with-data node "scroll-container")
|
||||||
parent-node (dom/get-parent-at node 2)
|
parent-node (dom/get-parent-at node 2)
|
||||||
first-child-node (dom/get-first-child parent-node)
|
first-child-node (dom/get-first-child parent-node)
|
||||||
@@ -392,61 +362,6 @@
|
|||||||
#(when (some? subid)
|
#(when (some? subid)
|
||||||
(rx/dispose! subid))))
|
(rx/dispose! subid))))
|
||||||
|
|
||||||
;; Setup scroll-driven lazy loading when expanded
|
|
||||||
;; and ensures selected children are loaded immediately
|
|
||||||
(mf/with-effect [expanded? (:shapes item) selected]
|
|
||||||
(let [shapes-vec (:shapes item)
|
|
||||||
total (count shapes-vec)]
|
|
||||||
(if expanded?
|
|
||||||
(let [;; Children are rendered in reverse order, so index 0 in render = last in shapes-vec
|
|
||||||
;; Find if any selected id is a direct child and get its render index
|
|
||||||
selected-child-render-idx
|
|
||||||
(when (and (> total chunk-size) (seq selected))
|
|
||||||
(let [shapes-reversed (vec (reverse shapes-vec))]
|
|
||||||
(some (fn [sel-id]
|
|
||||||
(let [idx (.indexOf shapes-reversed sel-id)]
|
|
||||||
(when (>= idx 0) idx)))
|
|
||||||
selected)))
|
|
||||||
;; Load at least enough to include the selected child plus extra
|
|
||||||
;; for context (so it can be centered in the scroll view)
|
|
||||||
min-count (if selected-child-render-idx
|
|
||||||
(+ selected-child-render-idx chunk-size)
|
|
||||||
chunk-size)
|
|
||||||
current @children-count*
|
|
||||||
new-count (min total (max current chunk-size min-count))]
|
|
||||||
(reset! children-count* new-count))
|
|
||||||
(reset! children-count* 0)))
|
|
||||||
(fn []
|
|
||||||
(when-let [obs ^js @observer-var]
|
|
||||||
(.disconnect obs)
|
|
||||||
(reset! observer-var nil))))
|
|
||||||
|
|
||||||
;; Re-observe sentinel whenever children-count changes (sentinel moves)
|
|
||||||
(mf/with-effect [children-count expanded?]
|
|
||||||
(let [total (count (:shapes item))
|
|
||||||
node (mf/ref-val ref)
|
|
||||||
scroll-node (dom/get-parent-with-data node "scroll-container")
|
|
||||||
lazy-node (mf/ref-val lazy-ref)]
|
|
||||||
;; Disconnect previous observer
|
|
||||||
(when-let [obs ^js @observer-var]
|
|
||||||
(.disconnect obs)
|
|
||||||
(reset! observer-var nil))
|
|
||||||
;; Setup new observer if there are more children to load
|
|
||||||
(when (and expanded?
|
|
||||||
(< children-count total)
|
|
||||||
scroll-node
|
|
||||||
lazy-node)
|
|
||||||
(let [cb (fn [entries]
|
|
||||||
(when (and (seq entries)
|
|
||||||
(.-isIntersecting (first entries)))
|
|
||||||
;; Load next chunk when sentinel intersects
|
|
||||||
(let [current @children-count*
|
|
||||||
next-count (min total (+ current chunk-size))]
|
|
||||||
(reset! children-count* next-count))))
|
|
||||||
observer (js/IntersectionObserver. cb #js {:root scroll-node})]
|
|
||||||
(.observe observer lazy-node)
|
|
||||||
(reset! observer-var observer)))))
|
|
||||||
|
|
||||||
[:& layer-item-inner
|
[:& layer-item-inner
|
||||||
{:ref dref
|
{:ref dref
|
||||||
:item item
|
:item item
|
||||||
@@ -471,32 +386,24 @@
|
|||||||
:on-enable-drag enable-drag
|
:on-enable-drag enable-drag
|
||||||
:on-disable-drag disable-drag
|
:on-disable-drag disable-drag
|
||||||
:on-toggle-visibility toggle-visibility
|
:on-toggle-visibility toggle-visibility
|
||||||
:on-toggle-blocking toggle-blocking
|
:on-toggle-blocking toggle-blocking}
|
||||||
:style style}
|
|
||||||
|
|
||||||
(when (and render-children?
|
(when (and (:shapes item) expanded?)
|
||||||
(:shapes item)
|
|
||||||
expanded?)
|
|
||||||
[:div {:class (stl/css-case
|
[:div {:class (stl/css-case
|
||||||
:element-children true
|
:element-children true
|
||||||
:parent-selected selected?
|
:parent-selected selected?
|
||||||
:sticky-children parent-board?)
|
:sticky-children parent-board?)
|
||||||
:data-testid (dm/str "children-" id)}
|
:data-testid (dm/str "children-" id)}
|
||||||
(let [all-children (reverse (d/enumerate (:shapes item)))
|
(for [[index id] (reverse (d/enumerate (:shapes item)))]
|
||||||
visible (take children-count all-children)]
|
(when-let [item (get objects id)]
|
||||||
(for [[index id] visible]
|
[:& layer-item
|
||||||
(when-let [item (get objects id)]
|
{:item item
|
||||||
[:& layer-item
|
:highlighted highlighted
|
||||||
{:item item
|
:selected selected
|
||||||
:highlighted highlighted
|
:index index
|
||||||
:selected selected
|
:objects objects
|
||||||
:index index
|
:key (dm/str id)
|
||||||
:objects objects
|
:sortable? sortable?
|
||||||
:key (dm/str id)
|
:depth depth
|
||||||
:sortable? sortable?
|
:parent-size parent-size
|
||||||
:depth depth
|
:component-child? component-tree?}]))])]))
|
||||||
:parent-size parent-size
|
|
||||||
:component-child? component-tree?}])))
|
|
||||||
(when (< children-count (count (:shapes item)))
|
|
||||||
[:div {:ref lazy-ref
|
|
||||||
:style {:min-height 1}}])])]))
|
|
||||||
|
|||||||
@@ -116,29 +116,13 @@
|
|||||||
(->> (dm/get-in grid-edition [edition :selected])
|
(->> (dm/get-in grid-edition [edition :selected])
|
||||||
(map #(dm/get-in objects [edition :layout-grid-cells %])))
|
(map #(dm/get-in objects [edition :layout-grid-cells %])))
|
||||||
|
|
||||||
shapes-with-children*
|
|
||||||
(mf/use-state nil)
|
|
||||||
|
|
||||||
_ (mf/use-effect
|
|
||||||
(mf/deps selected objects shapes)
|
|
||||||
(fn []
|
|
||||||
(reset! shapes-with-children* nil)
|
|
||||||
(let [result
|
|
||||||
(loop [queue (into #queue [] selected)
|
|
||||||
visited selected]
|
|
||||||
(if-let [id (peek queue)]
|
|
||||||
(let [shape (get objects id)
|
|
||||||
children (:shapes shape)]
|
|
||||||
(if (seq children)
|
|
||||||
(let [new-children (remove visited children)]
|
|
||||||
(recur (into (pop queue) new-children)
|
|
||||||
(into visited new-children)))
|
|
||||||
(recur (pop queue) visited)))
|
|
||||||
(sequence (keep (d/getf objects)) visited)))]
|
|
||||||
(reset! shapes-with-children* result))))
|
|
||||||
|
|
||||||
shapes-with-children
|
shapes-with-children
|
||||||
(deref shapes-with-children*)
|
(mf/with-memo [selected objects shapes]
|
||||||
|
(let [xform (comp (remove nil?)
|
||||||
|
(mapcat #(cfh/get-children-ids objects %)))
|
||||||
|
selected (into selected xform selected)]
|
||||||
|
(sequence (keep (d/getf objects)) selected)))
|
||||||
|
|
||||||
|
|
||||||
total-selected
|
total-selected
|
||||||
(count selected)]
|
(count selected)]
|
||||||
|
|||||||
@@ -55,7 +55,6 @@
|
|||||||
(-> (deref tokens)
|
(-> (deref tokens)
|
||||||
(select-keys (get tk/tokens-by-input name))
|
(select-keys (get tk/tokens-by-input name))
|
||||||
(not-empty))))
|
(not-empty))))
|
||||||
|
|
||||||
on-detach-attr
|
on-detach-attr
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-detach name)
|
(mf/deps on-detach name)
|
||||||
|
|||||||
@@ -507,7 +507,7 @@
|
|||||||
|
|
||||||
mdata {:on-error #(do
|
mdata {:on-error #(do
|
||||||
(reset! key* (uuid/next))
|
(reset! key* (uuid/next))
|
||||||
(st/emit! (ntf/warn error-msg)))}
|
(st/emit! (ntf/error error-msg)))}
|
||||||
params {:shapes shapes :pos pos :val val}]
|
params {:shapes shapes :pos pos :val val}]
|
||||||
(st/emit! (dwv/variants-switch (with-meta params mdata)))))))
|
(st/emit! (dwv/variants-switch (with-meta params mdata)))))))
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,17 @@
|
|||||||
(:require
|
(:require
|
||||||
[app.common.data :as d]
|
[app.common.data :as d]
|
||||||
[app.common.data.macros :as dm]
|
[app.common.data.macros :as dm]
|
||||||
|
[app.common.types.token :as tk]
|
||||||
[app.main.data.workspace :as dw]
|
[app.main.data.workspace :as dw]
|
||||||
[app.main.data.workspace.shapes :as dwsh]
|
[app.main.data.workspace.shapes :as dwsh]
|
||||||
|
[app.main.data.workspace.tokens.application :as dwta]
|
||||||
[app.main.features :as features]
|
[app.main.features :as features]
|
||||||
[app.main.store :as st]
|
[app.main.store :as st]
|
||||||
[app.main.ui.components.numeric-input :refer [numeric-input*]]
|
[app.main.ui.components.numeric-input :as deprecated-input]
|
||||||
[app.main.ui.components.select :refer [select]]
|
[app.main.ui.components.select :refer [select]]
|
||||||
|
[app.main.ui.context :as muc]
|
||||||
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
|
||||||
|
[app.main.ui.ds.controls.numeric-input :refer [numeric-input*]]
|
||||||
[app.main.ui.ds.foundations.assets.icon :as i]
|
[app.main.ui.ds.foundations.assets.icon :as i]
|
||||||
[app.render-wasm.api :as wasm.api]
|
[app.render-wasm.api :as wasm.api]
|
||||||
[app.util.i18n :as i18n :refer [tr]]
|
[app.util.i18n :as i18n :refer [tr]]
|
||||||
@@ -39,11 +43,16 @@
|
|||||||
(defn- check-layer-menu-props
|
(defn- check-layer-menu-props
|
||||||
[old-props new-props]
|
[old-props new-props]
|
||||||
(let [old-values (unchecked-get old-props "values")
|
(let [old-values (unchecked-get old-props "values")
|
||||||
new-values (unchecked-get new-props "values")]
|
new-values (unchecked-get new-props "values")
|
||||||
|
|
||||||
|
old-applied-tokens (unchecked-get old-props "appliedTokens")
|
||||||
|
new-applied-tokens (unchecked-get new-props "appliedTokens")]
|
||||||
(and (identical? (unchecked-get old-props "class")
|
(and (identical? (unchecked-get old-props "class")
|
||||||
(unchecked-get new-props "class"))
|
(unchecked-get new-props "class"))
|
||||||
(identical? (unchecked-get old-props "ids")
|
(identical? (unchecked-get old-props "ids")
|
||||||
(unchecked-get new-props "ids"))
|
(unchecked-get new-props "ids"))
|
||||||
|
(identical? old-applied-tokens
|
||||||
|
new-applied-tokens)
|
||||||
(identical? (get old-values :opacity)
|
(identical? (get old-values :opacity)
|
||||||
(get new-values :opacity))
|
(get new-values :opacity))
|
||||||
(identical? (get old-values :blend-mode)
|
(identical? (get old-values :blend-mode)
|
||||||
@@ -53,12 +62,54 @@
|
|||||||
(identical? (get old-values :hidden)
|
(identical? (get old-values :hidden)
|
||||||
(get new-values :hidden)))))
|
(get new-values :hidden)))))
|
||||||
|
|
||||||
|
(mf/defc numeric-input-wrapper*
|
||||||
|
{::mf/private true}
|
||||||
|
[{:keys [values name applied-tokens align on-detach] :rest props}]
|
||||||
|
(let [tokens (mf/use-ctx muc/active-tokens-by-type)
|
||||||
|
tokens (mf/with-memo [tokens name]
|
||||||
|
(delay
|
||||||
|
(-> (deref tokens)
|
||||||
|
(select-keys (get tk/tokens-by-input name))
|
||||||
|
(not-empty))))
|
||||||
|
|
||||||
|
on-detach-attr
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-detach name)
|
||||||
|
#(on-detach % name))
|
||||||
|
|
||||||
|
applied-token (get applied-tokens name)
|
||||||
|
opacity-value (or (get values name) 1)
|
||||||
|
|
||||||
|
props (mf/spread-props props
|
||||||
|
{:placeholder (if (or (= :multiple (:applied-tokens values))
|
||||||
|
(= :multiple opacity-value))
|
||||||
|
(tr "settings.multiple")
|
||||||
|
"--")
|
||||||
|
:applied-token applied-token
|
||||||
|
:tokens (if (delay? tokens) @tokens tokens)
|
||||||
|
:align align
|
||||||
|
:on-detach on-detach-attr
|
||||||
|
:name name
|
||||||
|
:value (* 100 opacity-value)})]
|
||||||
|
[:> numeric-input* props]))
|
||||||
|
|
||||||
(mf/defc layer-menu*
|
(mf/defc layer-menu*
|
||||||
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
{::mf/wrap [#(mf/memo' % check-layer-menu-props)]}
|
||||||
[{:keys [ids values]}]
|
[{:keys [ids values applied-tokens]}]
|
||||||
(let [hidden? (get values :hidden)
|
(let [token-numeric-inputs
|
||||||
|
(features/use-feature "tokens/numeric-input")
|
||||||
|
|
||||||
|
hidden? (get values :hidden)
|
||||||
blocked? (get values :blocked)
|
blocked? (get values :blocked)
|
||||||
|
|
||||||
|
on-detach-token
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps ids)
|
||||||
|
(fn [token attr]
|
||||||
|
(st/emit! (dwta/unapply-token {:token (first token)
|
||||||
|
:attributes #{attr}
|
||||||
|
:shape-ids ids}))))
|
||||||
|
|
||||||
current-blend-mode (or (get values :blend-mode) :normal)
|
current-blend-mode (or (get values :blend-mode) :normal)
|
||||||
current-opacity (opacity->string (:opacity values))
|
current-opacity (opacity->string (:opacity values))
|
||||||
|
|
||||||
@@ -118,6 +169,17 @@
|
|||||||
(let [value (/ value 100)]
|
(let [value (/ value 100)]
|
||||||
(on-change ids :opacity value))))
|
(on-change ids :opacity value))))
|
||||||
|
|
||||||
|
on-opacity-change
|
||||||
|
(mf/use-fn
|
||||||
|
(mf/deps on-change handle-opacity-change)
|
||||||
|
(fn [value]
|
||||||
|
(if (or (string? value) (int? value))
|
||||||
|
(handle-opacity-change value)
|
||||||
|
(do
|
||||||
|
(st/emit! (dwta/toggle-token {:token (first value)
|
||||||
|
:attrs #{:opacity}
|
||||||
|
:shape-ids ids}))))))
|
||||||
|
|
||||||
handle-set-hidden
|
handle-set-hidden
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps ids)
|
(mf/deps ids)
|
||||||
@@ -176,8 +238,9 @@
|
|||||||
preview-complete?))
|
preview-complete?))
|
||||||
(swap! state* assoc :selected-blend-mode current-blend-mode)))
|
(swap! state* assoc :selected-blend-mode current-blend-mode)))
|
||||||
|
|
||||||
[:div {:class (stl/css-case :element-set-content true
|
[:section {:class (stl/css-case :element-set-content true
|
||||||
:hidden hidden?)}
|
:hidden hidden?)
|
||||||
|
:aria-label "layer-menu-section"}
|
||||||
[:div {:class (stl/css :select)}
|
[:div {:class (stl/css :select)}
|
||||||
[:& select
|
[:& select
|
||||||
{:default-value selected-blend-mode
|
{:default-value selected-blend-mode
|
||||||
@@ -187,16 +250,34 @@
|
|||||||
:class (stl/css-case :hidden-select hidden?)
|
:class (stl/css-case :hidden-select hidden?)
|
||||||
:on-pointer-enter-option handle-blend-mode-enter
|
:on-pointer-enter-option handle-blend-mode-enter
|
||||||
:on-pointer-leave-option handle-blend-mode-leave}]]
|
:on-pointer-leave-option handle-blend-mode-leave}]]
|
||||||
[:div {:class (stl/css :input)
|
|
||||||
:title (tr "workspace.options.opacity")}
|
|
||||||
[:span {:class (stl/css :icon)} "%"]
|
|
||||||
[:> numeric-input*
|
(if token-numeric-inputs
|
||||||
{:value current-opacity
|
|
||||||
:placeholder "--"
|
[:> numeric-input-wrapper*
|
||||||
:on-change handle-opacity-change
|
{:on-change on-opacity-change
|
||||||
:min 0
|
:on-detach on-detach-token
|
||||||
:max 100
|
:icon i/percentage
|
||||||
:className (stl/css :numeric-input)}]]
|
:min 0
|
||||||
|
:max 100
|
||||||
|
:name :opacity
|
||||||
|
:property (tr "workspace.options.opacity")
|
||||||
|
:applied-tokens applied-tokens
|
||||||
|
:align :right
|
||||||
|
:class (stl/css :numeric-input-wrapper)
|
||||||
|
:values values}]
|
||||||
|
|
||||||
|
[:div {:class (stl/css :input)
|
||||||
|
:title (tr "workspace.options.opacity")}
|
||||||
|
[:span {:class (stl/css :icon)} "%"]
|
||||||
|
[:> deprecated-input/numeric-input*
|
||||||
|
{:value current-opacity
|
||||||
|
:placeholder "--"
|
||||||
|
:on-change handle-opacity-change
|
||||||
|
:min 0
|
||||||
|
:max 100
|
||||||
|
:className (stl/css :numeric-input)}]])
|
||||||
|
|
||||||
|
|
||||||
[:div {:class (stl/css :actions)}
|
[:div {:class (stl/css :actions)}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
@use "refactor/common-refactor.scss" as deprecated;
|
@use "refactor/common-refactor.scss" as deprecated;
|
||||||
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
@use "../../../sidebar/common/sidebar.scss" as sidebar;
|
||||||
|
@use "ds/_utils.scss" as *;
|
||||||
|
|
||||||
.element-set-content {
|
.element-set-content {
|
||||||
@include sidebar.option-grid-structure;
|
@include sidebar.option-grid-structure;
|
||||||
@@ -43,3 +44,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.numeric-input-wrapper {
|
||||||
|
grid-column: span 2;
|
||||||
|
--dropdown-width: var(--7-columns-dropdown-width);
|
||||||
|
--dropdown-offset: #{px2rem(-35)};
|
||||||
|
}
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
|
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
|
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -100,6 +100,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type shape-type
|
:type shape-type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
:applied-tokens applied-tokens
|
:applied-tokens applied-tokens
|
||||||
|
|||||||
@@ -111,6 +111,7 @@
|
|||||||
[:div {:class (stl/css :options)}
|
[:div {:class (stl/css :options)}
|
||||||
[:> layer-menu* {:type type
|
[:> layer-menu* {:type type
|
||||||
:ids layer-ids
|
:ids layer-ids
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:type type
|
[:> measures-menu* {:type type
|
||||||
:ids measure-ids
|
:ids measure-ids
|
||||||
|
|||||||
@@ -382,7 +382,7 @@
|
|||||||
objects
|
objects
|
||||||
objects)))
|
objects)))
|
||||||
|
|
||||||
[layer-ids layer-values]
|
[layer-ids layer-values layer-tokens]
|
||||||
(get-attrs shapes objects :layer)
|
(get-attrs shapes objects :layer)
|
||||||
|
|
||||||
[text-ids text-values]
|
[text-ids text-values]
|
||||||
@@ -406,7 +406,7 @@
|
|||||||
[exports-ids exports-values]
|
[exports-ids exports-values]
|
||||||
(get-attrs shapes objects :exports)
|
(get-attrs shapes objects :exports)
|
||||||
|
|
||||||
[layout-container-ids layout-container-values layout-contianer-tokens]
|
[layout-container-ids layout-container-values layout-container-tokens]
|
||||||
(get-attrs shapes objects :layout-container)
|
(get-attrs shapes objects :layout-container)
|
||||||
|
|
||||||
[layout-item-ids layout-item-values {}]
|
[layout-item-ids layout-item-values {}]
|
||||||
@@ -442,6 +442,7 @@
|
|||||||
(when-not (empty? layer-ids)
|
(when-not (empty? layer-ids)
|
||||||
[:> layer-menu* {:type type
|
[:> layer-menu* {:type type
|
||||||
:ids layer-ids
|
:ids layer-ids
|
||||||
|
:applied-tokens layer-tokens
|
||||||
:values layer-values}])
|
:values layer-values}])
|
||||||
|
|
||||||
(when-not (empty? measure-ids)
|
(when-not (empty? measure-ids)
|
||||||
@@ -459,7 +460,7 @@
|
|||||||
{:type type
|
{:type type
|
||||||
:ids layout-container-ids
|
:ids layout-container-ids
|
||||||
:values layout-container-values
|
:values layout-container-values
|
||||||
:applied-tokens layout-contianer-tokens
|
:applied-tokens layout-container-tokens
|
||||||
:multiple true}]
|
:multiple true}]
|
||||||
|
|
||||||
(when (or is-layout-child? has-flex-layout-container?)
|
(when (or is-layout-child? has-flex-layout-container?)
|
||||||
|
|||||||
@@ -84,6 +84,7 @@
|
|||||||
|
|
||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:type type
|
:type type
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu* {:ids ids
|
[:> measures-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
|||||||
@@ -125,6 +125,7 @@
|
|||||||
[:*
|
[:*
|
||||||
[:> layer-menu* {:ids ids
|
[:> layer-menu* {:ids ids
|
||||||
:type type
|
:type type
|
||||||
|
:applied-tokens applied-tokens
|
||||||
:values layer-values}]
|
:values layer-values}]
|
||||||
[:> measures-menu*
|
[:> measures-menu*
|
||||||
{:ids ids
|
{:ids ids
|
||||||
|
|||||||
@@ -7797,10 +7797,6 @@ msgstr "Line Height depends on Font Size. Add a Font Size to get the resolved va
|
|||||||
msgid "workspace.tokens.create-new-theme"
|
msgid "workspace.tokens.create-new-theme"
|
||||||
msgstr "Create your first theme now."
|
msgstr "Create your first theme now."
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs
|
|
||||||
msgid "workspace.tokens.warning-name-change"
|
|
||||||
msgstr "Renaming this token will break any reference to its old name"
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/sets/lists.cljs:96, src/app/main/ui/workspace/tokens/themes.cljs:44
|
#: src/app/main/ui/workspace/tokens/sets/lists.cljs:96, src/app/main/ui/workspace/tokens/themes.cljs:44
|
||||||
msgid "workspace.tokens.create-one"
|
msgid "workspace.tokens.create-one"
|
||||||
msgstr "Create one."
|
msgstr "Create one."
|
||||||
|
|||||||
@@ -4575,10 +4575,6 @@ msgstr "Mostrar/ocultar recursos"
|
|||||||
msgid "shortcuts.toggle-colorpalette"
|
msgid "shortcuts.toggle-colorpalette"
|
||||||
msgstr "Mostrar/ocultar paleta de colores"
|
msgstr "Mostrar/ocultar paleta de colores"
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/tokens/management/forms/generic_form.cljs
|
|
||||||
msgid "workspace.tokens.warning-name-change"
|
|
||||||
msgstr "Cambiar el nombre de este token romperá cualquier referencia a su nombre anterior."
|
|
||||||
|
|
||||||
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
|
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
|
||||||
msgid "shortcuts.toggle-focus-mode"
|
msgid "shortcuts.toggle-focus-mode"
|
||||||
msgstr "Mostrar/ocultar focus mode"
|
msgstr "Mostrar/ocultar focus mode"
|
||||||
|
|||||||